People are weird. They would use your software in ways you cannot imagine. And they will break your software when they do this.
You need to think about how they may break your software and code for these use cases. I call them edge cases.
There are a lot of edge cases for the calculator (as you’ll see in the next lesson). It gets confusing quickly when we need to handle edge cases. So before we tackle edge cases, we will take a detour and test the happy path.
(Testing would turn out to be a shortcut instead).
Note: If you’d like a challenge, go ahead and build edge cases without writing tests. I guarantee you’ll start 🤬-ing and 😱-ing soon enough. 😛.
Testing with console.assert
The console object has an assert method that lets you make assertions. Here’s the syntax:
console.assert(assertion, message)When you make an assertion (in programming), you state an expression that evaluates to true. If the expression evaluates to true, the test passes. And nothing will be shown in the console.
If the expression evaluates to false, the test fails and you get an error.
console.assert(1 === 1) // true. Assertion passes. No error.console.assert(1 === 2) // false. Assertion fails. Has error.
You can give yourself more information about the test with the message argument. This message will show up when the test fails.
console.assert(1 === 2, '1 should not be equal to 2')
Creating your first test
Think about what you want to test. For a calculator, we want to make sure the calculator shows the correct result no matter what users punch in.
Let’s make things simple for our first test. If a user presses 2, we want to check whether the calculator shows 2.
We can make JavaScript press a button with the click method. If we want the user to press 2, we need to select the 2 button and use click on it.
const buttonTwo = calculator.querySelector('[data-key="2"]')buttonTwo.click()If you refresh the page, you should see the calculator shows 2. This is because we used JavaScript to click the button that says 2.
Now we want to make sure the calculator says 2. We do this with console.assert.
Here, we need to get the displayed result from the calculator first. After getting the displayed result, we check whether this displayed result is 2.
const result = calculator.querySelector('.calculator__display').textContentconsole.assert(result === 2, 'Number key')
The test failed. But why?
It failed because result (a String) is not strictly equal to 2 (a Number). If we change 2 to a String, the test should pass, and no errors would show up in the console.
const result = calculator.querySelector('.calculator__display').textContentconsole.assert(result === '2', 'Number key')Resetting the calculator
We need to reset the calculator after each test. This ensures results from the current test doesn’t affect the next test.
To reset the calculator, we press the clear button twice.
// Reset calculatorconst clearKey = calculator.querySelector('[data-key="clear"]')clearKey.click()clearKey.click()We’ll make another test to ensure the calculator is cleared here.
const resultAfterClear = calculator.querySelector( '.calculator__display',).textContentconsole.assert(result === '0', 'Calculator cleared')If the calculator is cleared, it should not have dataset.firstValue and dataset.operator. We’ll ensure they don’t exist.
console.assert(!calculator.dataset.firstValue, 'No first value')console.assert(!calculator.dataset.operator, 'No operator value')Creating helper functions
We need to click calculator buttons many times throughout our tests. We should create a function to click a button. It’ll make things easier for us down the road.
function pressKey(key) { calculator.querySelector(`[data-key="${key}"]`).click()}We need to get the displayed value many times. Let’s create a function to help us here.
function getDisplayValue() { return calculator.querySelector('.calculator__display').textContent}We also need to reset the calculator after each test. Let’s create a function too.
function resetCalculator() { pressKey('clear') pressKey('clear')
console.assert(getDisplayValue() === '0', 'Calculator cleared') console.assert(!calculator.dataset.firstValue, 'No first value') console.assert(!calculator.dataset.operator, 'No operator value')}Our test should look like this now:
// Make sure calculator shows numberpressKey('2')console.assert(getDisplayValue() === '2', 'Number key')resetCalculator()Some more tests
Let’s say the user presses 3, then 5. We want to make sure the calculator shows 35.
pressKey('3')pressKey('5')console.assert(getDisplayValue() === '35', 'Number Number')resetCalculator()If the user presses 4, then ., the calculator should show 4..
pressKey('4')pressKey('decimal')console.assert(getDisplayValue() === '4.', 'Number Decimal')resetCalculator()If the user presses 4, ., 5, the calculator should show 4.5.
pressKey('4')pressKey('decimal')pressKey('5')console.assert(getDisplayValue() === '4.5', 'Number Decimal Number')resetCalculator()Creating another helper
We used pressKey many times now. Each pressKey stays on its a new line now, but wouldn’t it make sense to put all our key presses in a single line?
Let’s create a function to help us write keypresses. Ideally, we want to to use the function like this:
pressKeys('4', 'decimal', '5')In pressKeys, we can use the rest operator to pack all arguments into an array. Then, we run pressKey over each item in the array.
function pressKeys(...keys) { keys.forEach(key => pressKey(key))}We can simplify the function a little since pressKey takes in one variable only:
function pressKeys(...keys) { keys.forEach(pressKey)}Our test should look like this now:
// Test 1pressKeys('2')console.assert(getDisplayValue() === '2', 'Number key')resetCalculator()
// Test 2pressKeys('3', '5')console.assert(getDisplayValue() === '35', 'Number Number')resetCalculator()
// Test 3pressKeys('4', 'decimal')console.assert(getDisplayValue() === '4.', 'Number Decimal')resetCalculator()
// Test 4pressKeys('4', 'decimal', '5')console.assert(getDisplayValue() === '4.5', 'Number Decimal Number')resetCalculator()Another helper to run tests
We have to write console.assert and reset calculator for every test. This is okay, but it gets tedious. We can create a function called runTest that runs each test.
function runTest() { // ...}We’ll copy one of our tests into runTest first.
function runTest() { pressKeys('4', 'decimal', '5') console.assert(getDisplayValue() === '4.5', 'Number Decimal Number') resetCalculator()}We know runTest needs three things:
- The keys to press
- The expected result
- A message for each test
We can do this:
function runTest(result, message, ...keys) { pressKeys(...keys) console.assert(getDisplayValue() === result, message) resetCalculator()}But this code is not friendly. We need to remember the order of arguments we pass in. We can simplify things by passing in an array of keys instead.
function runTest(result, message, keys) { pressKeys(...keys) console.assert(getDisplayValue() === result, message) resetCalculator()}We can simplify things even further by asking them to pass in an object.
function runTest(test) { pressKeys(...test.keys) console.assert(getDisplayValue() === test.result, test.message) resetCalculator()}We can run each test like this:
runTest({ message: 'Number Decimal Number', keys: ['4', 'decimal', '5'], result: '4.5',})This new format makes more sense than simply writing console.assert. It helps the brain read what’s happening since we defined it this way:
- What we call the test (
message) - What keys we’re testing (
keys) - What we expect to see (
result)
Since we have many tests, we can create a tests array to contain our tests. We’ll loop through this test array and use runTest on each test.
const tests = [ { message: 'Number key', keys: ['2'], result: '2', }, { message: 'Number Number', keys: ['3', '5'], result: '35', }, { message: 'Number Decimal', keys: ['4', 'decimal'], result: '4.', }, { message: 'Number Decimal Number', keys: ['4', 'decimal', '5'], result: '4.5', },]
// Runs teststests.forEach(runTest)Testing calculations
Next, we want to make sure our calculator works. We need to test addition, subtraction, multiplication, and division.
You should be able to get to this without much difficulty:
const tests = [ // Initial Expressions // ...
// Calculations { message: 'Addition', keys: ['2', 'plus', '5', 'equal'], result: '7', }, { message: 'Subtraction', keys: ['5', 'minus', '9', 'equal'], result: '-4', }, { message: 'Multiplication', keys: ['4', 'times', '8', 'equal'], result: '32', }, { message: 'Division', keys: ['5', 'divide', '1', '0', 'equal'], result: '0.5', },]Testing the clear key
The clear key has two functions:
- Press once: Clear the display ONLY
- Press twice: Clear everything
We already tested the “press twice” version with resetCalculator. What’s left is to create tests for the “press once” version.
There are two possibilities here:
- Before a calculation
- After a calculation
Let’s start by creating a function to test the clear key.
function testClearKey() { // ...}Before a calculation
Let’s say the user presses 5, then clear. Two things should happen:
- Display should change from
5to0. - Clear key should change from
CEtoAC.
First, we’ll make testClearKey press 5 and clear.
function testClearKey() { // Before calculation pressKeys('5', 'clear')}We want to make sure the calculator display changed to 0. This should be easy for you because we’ve been doing this the entire lesson.
function testClearKey() { // Before calculation pressKeys('5', 'clear') console.assert(getDisplayValue() === '0', 'Clear before calculation')}Next, we want to make sure the clear key says AC.
function testClearKey() { // Before calculation pressKeys('5', 'clear') const clearKeyText = calculator.querySelector('[data-key="clear"]').textContent console.assert(getDisplayValue() === '0', 'Clear before calculation') console.assert(clearKeyText === 'AC', 'Clear once, should show AC')}Finally, we make sure to reset the calculator for later tests.
function testClearKey() { // Before calculation // ... resetCalculator()}Remember to run testClearKey in your code!
testClearKey()After a calculation
Let’s say the user pressed 5, times, 9, equal, clear. What should happen? In this case, four things should happen:
- Display should show
0. - Clear key should show
AC calculator.dataset.firstValueshould not be reset.calculator.dataset.operatorshould not be reset.
We already covered the first two changes in before a calculation, so we only need to test the last changes.
First, we’ll make testClearKey press 5, times, 9, equal, clear.
function testClearKey() { // ... // After calculation pressKeys('5', 'times', '9', 'equal', 'clear')}Next, we need to ensure firstValue and operator did not get reset. We can do this with a truthy expression.
function testClearKey() { // ... // After calculation pressKeys('5', 'times', '9', 'equal', 'clear') const { firstValue, operator } = calculator.dataset console.assert(firstValue, 'Clear once; should have first value') console.assert(operator, 'Clear once; should have operator value')}Finally, we reset the calculator again.
function testClearKey() { // ... // After calculation // ... resetCalculator()}That’s it!
Welcome! Unfortunately, you don’t have access to this lesson. To get access, please purchase the course or enroll in Magical Dev School.
Unlock this lesson