Introduction to front-end testing. Part two. Unit Testing
As we decided in the first part , a unit test is a code that tests units (parts) of the code: functions, modules, or classes. Most people think that the bulk of testing should be unit tests – but not me. Throughout the series of articles, I will argue that it is not the method of testing that matters, but their number. Tests should be enough to be sure of the quality of the product provided to the user.
Unit tests are the easiest tests to write and the easiest to understand. The whole point is to submit something to the input of the unit and check the result at the output (for example, you supply function parameters to the input, and get a value at the output).
We also recommend that you read our article on the purposes? of unit testing – you will find out why they are needed and what, besides testing, they can be used for.
Moreover, you should encourage yourself to write the code in a way that makes it?possible?to test those units in isolation, without needing to bring in other units.
Units in the Calculator App
But enough of theory — let’s look at the?Calculator?app. It’s a React application, which has two main components, the?keypad?and the?display. These are?definitely?units, as they rely on no other units, but they are React units, so we will devote a future post to figuring out how to test them.
The reason I don't use JSX is because I didn't want to go deep into code translation. All modern browsers are fully ES6 compliant, so why don't I run the code without transpiling? Yes, I know my code won't run in IE, but it's a demo code, so it's ok. In a real project, I wouldn't do that.
But some code somewhere has to determine what happens when clicking on a digit (e.g. “1”, “5”) or an operator (e.g. “+”, “=”)? As is customary today, I divided my components into presentational (keypad and display) and container components – calculator app. This is the only component in this application that has a state, and it is this component that determines what should be displayed on the screen whenever we click the calculator button.
The “calculator” module
But that component is only responsible for the display logic, but what about the calculations? This is handled by a separate module, calculator, which has no React dependencies. This module is perfect for unit testing! Code is ideal for unit testing if it does not contain I/O and UI dependencies. You should try to avoid such dependencies in your application logic.
What does I/O (input/output) mean in web applications? There are no files, databases, etc.? Yes, it doesn't, but it does have AJAX calls, localStorage, and DOM access. I believe that everything related to the browser API is I/O.
How did I separate the calculator logic from the React component? In the case of calculator, it’s pretty easy. The logic is very algorithmic. I separated it into the module?calculator.
The module is very simple – it takes the state of the calculator (an object) and a character (that is, a digit or an operator) and returns the new calculator state. If you've ever used Redux, you'll see that this is similar to the Redux reducer pattern. But if you always get a new calculator state from the previous one, how do you get the very first one? Simple – the module also exports the initialState that you use to initialize the calculator. The state of the calculator is not unknown – it includes a field called display, which is what the calculator application needs to show for this state.
If you want to see the code, let's take a look at the beginning, which is the most important part since the details of the algorithm are not that important:
module.exports.initialState = { display: '0', initial: true }
module.exports.nextState = (calculatorState, character) => {
??if (isDigit(character)) {
????return addDigit(calculatorState, character)
??} else if (isOperator(character)) {
????return addOperator(calculatorState, character)
??} else if (isEqualSign(character)) {
????return compute(calculatorState)
??} else {
????return calculatorState
??}
}
//....
The specifics of the algorithm are not that important, but what?is?important is that the function the module exports is very simple — given a state, we can always check what the next state is.
And this is what we do in?test-calculator – look at the code below.
const {describe, it} = require('mocha')
const {expect} = require('chai')
const calculator = require('../../lib/calculator')
describe('calculator', function () {
? const stream = (characters, calculatorState = calculator.initialState) =>
? ? !characters
? ? ? ? calculatorState
? ? ? : stream(characters.slice(1),
? ? ? ? calculator.nextState(calculatorState, characters[0]))
? it('should show initial display correctly', () => {
? ? expect(calculator.initialState.display).to.equal('0')
? })
? it('should replace 0 in initialState', () => {
? ? expect(stream('4').display).to.equal('4')
? })
? it('should add a digit if not in initial state', () => {
? ? expect(stream('34').display).to.equal('34')
? })
? it('should not change display if operator appears', () => {
? ? expect(stream('3+').display).to.equal('3')
? })
? it('should change display to digit when digit appears after operator', () => {
? ? expect(stream('37+4').display).to.equal('4')
? })
? it('should compute 37+42= to be 79', () => {
? ? expect(stream('37+42=').display).to.equal('79')
? })
? it('should compute another expression after "="', () => {
? ? expect(stream('1+2=4*5=').display).to.equal('20')
? })
? it('should enabling using computation result in next computation', () => {
? ? expect(stream('1+2=*5=').display).to.equal('15')
? })
? it('second operator is also an equal', () => {
? ? expect(stream('1+2*').display).to.equal('3')
? })
? it('second operator is also an equal but it can continue after that', () => {
? ? expect(stream('1+2*11=').display).to.equal('33')
? })
? it('+42= should compute to 42', () => {
? ? expect(stream('+42=').display).to.equal('42')
? })
? it('*42= should compute to 0', () => {
? ? expect(stream('*42=').display).to.equal('0')
? })
? it('47-48= should compute to -1', () => {
? ? expect(stream('47-48=').display).to.equal('-1')
? })
? it('8/2= should compute to 4', () => {
? ? expect(stream('8/2=').display).to.equal('4')
? })
})
This is where this logic, which is not trivial, is fully tested. And how do we test it? We always use a testing framework. The most popular framework is currently?Mocha, and we use that. But feel free to use Jest, Jasmine, Tape, or any other testing framework you find out there that you like.
Testing a Unit using Mocha
All testing frameworks are similar – you write test code in functions called test, and the framework runs them. The specific code that runs them is usually called "runner".
The "Runner" in Mocha is a script called mocha. If you look at package.json in the test script, you will see it there:
"scripts": {
...
????"test": "mocha 'test/**/test-*.js' && eslint test lib",
...
},
This will run all the tests in the test folder that start with the?test-?prefix. So that if you clone this repo, and?npm install?it, you can run?npm test?yourself to try it out.
领英推荐
As a matter of fact, the convention of having all tests in the?test?folder of the root of the package is a very strong convention in npm packages, so you should follow that if you want others to think you’re professional.
When running it, the output will be something like:
Obviously, if a test doesn’t pass, you will see some angry reds there, which you will?immediately?run to fix, right? Let’s have a look at it:
const {describe, it} = require('mocha')
const {expect} = require('chai')
const calculator = require('../../lib/calculator')
describe('calculator', function () {
??const stream = (characters, calculatorState = calculator.initialState) =>
????!characters
??????? calculatorState
??????: stream(characters.slice(1),
???????????????calculator.nextState(calculatorState, characters[0]))
??it('should show initial display correctly', () => {
????expect(calculator.initialState.display).to.equal('0')
??})
??it('should replace 0 in initialState', () => {
????expect(stream('4').display).to.equal('4')
??})
//...
First of all, we import mocha and the expect library for asserts.?
We've imported the functions we need: describe, it, and except.
Then we import the module we are testing, the calculator.
Then there are tests that are described using the it function, for example:
it('should show initial display correctly', () => {
????expect(calculator.initialState.display).to.equal('0')
})
This function takes a string describing the test and a function that is the test itself. But it tests cannot be "bare" – they must be in test groups, which are defined using the describe function.
And what is in the test function? Whatever we want. In this case, we are checking that the initial state of display is equal to 0. How do we do this? We really could do something like this:
if (calculator.initialState.display !== '0')
??throw 'failed'
This would have worked wonderfully! A test in Mocha fails if it throws an exception. It’s as simple as that. But?expect?makes it much nicer as it has lots of features that make testing stuff easier – things like checking that an array or object is equal to a specific value.
This is the essence of unit testing – running a function or set of functions (or creating an instance of an object and calling some of its methods, if we are talking about OOP) and comparing the actual result with the expected one.
Writing Testable Units
The hard part of unit testing is not writing tests, but separating the code as much as possible so that it is unit-testable. And?code that is unit-testable is code that has little dependencies on other modules, and does no I/O. And it's difficult because we tend to link logic with UI code and I/O code. But it is possible, and there are many ways to do it. For example, if your code is validating fields or a group of fields, you would combine all validation functions into a module and test it.
Wait, the code works under NodeJS?!
An incredibly important fact is that unit tests work under NodeJS! The application itself works in the browser, and for testing the code, including the final one, we use NodeJS.
This is possible because our code is isomorphic. This means that it works both in the browser and under NodeJS. How did it happen? If you write your code without using I/O, it means that it doesn't do anything browser-specific, so there's no reason why it wouldn't run under NodeJS. Especially if it uses require, since require is natively recognized by both NodeJS and a bundler like Webpack. And if you look at package.json, you'll see that we're using Webpack specifically to bundle code that uses require:
"scripts": {
???"build": "webpack && cp public/* dist",
???...
}
So our code uses require to import React and other modules, and thanks to the magic of NodeJS and Webpack, we can use this module system in both NodeJS and the browser – NodeJS recognizes require natively, while Webpack uses require to combine all modules into one. large JS file.
Running Unit Tests under the browser
By the way, we could use another test framework, Karma, to run our Mocha code under the browser. However, I believe that if unit tests can be run under Node, then that is the way to do it. And if you don't transpile the code, then they are very fast at execution.
But we can’t not run tests in the browser, because we don’t know if our code works in the browser. There may be differences in the behavior of JS code in the browser and in NodeJS. And here E2E tests come to the rescue, which we will talk about in the next part.