20
Basics of Javascript Test Driven Development (TDD) with Jest
Test Driven Development (TDD)'s main idea is to simply start working on code by writing automated tests BEFORE writing the code that is being tested. There are many test-running systems in Javascript: Jasmine, Jest, Tape, and Mocha to name a few. They have their special features but the syntax is very similar. The framework chosen should not be an issue because
writing tests is less about the syntax but more on the TDD philosophy,
so I tried internalizing the concepts using Jest. My main goal while doing the exercise is to know the why's and whats of testing.
Before diving in, here are some notes I took from this brilliant talk, The Magic of Testing.
- Why does most devs hate tests? Because they are slow and fragile and expensive (time).
- It is perfectly valid to delete some tests.
- Unit Test Goals: They must be thorough (we want them to prove logically and completely that the single object under test is behaving correctly) and stable (we dont want to break the test everytime the implementation detail is changed 😟), fast and few (write tests for the most parsimonious expression [mmmmmm 🤔]).
- Do not test private methods. But break this rule if it saves money during development.
- A mock is a test double, it plays the role of some object in your real app. Ensure test double stays in sync with the API.
- Trust collaborators that they will do the right thing. Insist on simplicity.
- Getting better at testing takes time and practice.
The object under test have three origin of messages :
📌 Incoming - messages to the object from outside
📌 Self - messages sent by the object under test to itself
📌 Outgoing - messsages sent by the object to the outside.
There are two types of messages : query and command. Queries return something or changes nothing. Command types return nothing but changes something.
The grid of test results below shows how each type of message can be unit tested.
Message Type | Query | Command |
---|---|---|
Incoming |
Assert result Test incoming query messages by making assertions about what they send back. Test the interface and not the implementation. |
Test incoming command messages by making assertions about direct public side effects. DRY it out. Receiver of incoming message has sole responsibility for asserting the result of direct public side effects. |
Sent to Self | Ignore: Do not test private methods. | Ignore: Do not test private methods. |
Outgoing | Ignore. The receiver of an incoming query is solely responsible for assertions that involve state. If a message has no visible side effects, the sender should not test it |
Expect to send outgoing command messages using mocks |
- Reduces bugs that may be introduced when adding new features or modifying existing features
- Builds a safety net against changes of other programmers that may affect a specific part of the code
- Reduces the cost of change by ensuring that the code will still work with the new changes
- Reduces the need for manual (monkey) checking by testers and developers
- Improves confidence in code
- Reduces the fear of breaking changes during refactors
Jest is a javascript testing framework focusing on simplicity but still ensures the correctness of the Javascript code base. It boasts itself to be fast and safe, reliably running tests in parallel with unique global state. To make things quick, Jest runs previously failed tests first and re-organizes runs based on how long test files take.
Moreover, Jest is very well-documented and requires little configuration. It indeed makes javascript testing delightful. It can be installed by using either yarn
or npm
.
- Obvious implementation. You write the test with the implementation since you know how to implement the method to test.
- Fake it til you make it. If you know the problem and solutions, but the way you code them up is not immediately obvious to you, then you can use a trick called "fake it 'til you make it."
- Triangulation . This is the most conservative way of doing TDD. If you don't even know the solution, you just get to green at all costs , red-green, red-green loop.
The simplest way to test a value is with exact equality.
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
The code snippet above returns an "expectation" object. The toBe(3)
portion is the matcher. When Jest runs, it tracks all the failing matchers so it can print nice error messages. The toBe
matcher uses Object.is
to test the equality.
In unit tests, the special values undefined
, null
, false
might be needed to be checked also. Jest contains helpers that lets developers be explicit with what to expect. It is then good to use a matcher that most precisely corresponds to what the code is doing.
-
toBeNull
matches onlynull
-
toBeUndefined
matches onlyundefined
-
toBeDefined
is the opposite oftoBeUndefined
-
toBeTruthy
matches anything that anif
statement treats as true -
toBeFalsy
matches anything that anif
statement treats as false
There are also Jest matchers for comparing numbers such as toBeGreaterThan
, toBeGreaterThanOrEqual
, toBeLessThan
, toBeLessThanOrEqual
. For floating point numbers, there are equality matcher like toBeCloseTo
.
Strings can be checked against regular expressions using toMatch
.
toContain
can be used to check if a particular item can be found in an array or iterable.
toThrow
can be used to check if a particular function throws a specific error. It is to be noted that the function being checked needs to be invoked within a wrapping function for the toThrow
exception to work.
There are also more advanced Jest matchers used for testing asynchronous code, i.e for callbacks and promises.
This is my first time writing javascript unit tests using Jest. It is quite new so I needed some practice 😄. I tried using the obvious implementation and triangulation mode of testing for some of the methods below. The full implementation of the methods and their corresponding tests can be found in my Jest practice github repository.
-
capitalize(string)
takes a string and returns that string with the first character capitalized.
const capitalize = require('../capitalize');
test('should capitalize lowercase string correctly', () => {
expect(capitalize("capitalize")).toBe("Capitalize");
});
test("should return '' for strings with length 0", () => {
expect(capitalize("")).toBe("");
});
// other tests here
-
reverseString(string)
takes a string and returns it reversed. Below is a snippet of the test I wrote for a normal scenario.
const reverseString = require('../reverse-string');
test('should reverse normal strings', () => {
expect(reverseString("reverse")).toBe("esrever");
});
//other tests here
- A
calculator
object that contains the basic operations:add
,subtract
,divide
, andmultiply
. The following test snippet below shows that the method will throw an error message if the divisor is zero.
const calculator = require("../calculator");
//other tests here
test("should throw an error if divisor is 0", () => {
expect(() => calculator.divide(20, 0)).toThrow("cannot divide by 0");
});
-
caesar cipher
. A caesar cipher is a substitution cipher where each letter in the text is shifted a certain places number down the alphabet. More info can be read here.
One thing to remember from this part of the exercise is that it is not needed to explicity test the smaller functions, just the public ones. If the larger function works then it must be the case that the helper methods are functioning well.
const caesar = require("../caesar-cipher");
//other tests here
test('wraps', function() {
expect(caesar('Z', 1)).toEqual('A');
});
test('works with large shift factors', function() {
expect(caesar('Hello, World!', 75)).toEqual('Ebiil, Tloia!');
});
test('works with large negative shift factors', function() {
expect(caesar('Hello, World!', -29)).toEqual('Ebiil, Tloia!');
});
- Array Analysis. This function takes an array of numbers and returns an object with the following properties:
average
,min
,max
, andlength
.
const analyze = require("../analyze");
const object = analyze([1,8,3,4,2,6]);
test("should return correct average", () => {
expect(object.average).toEqual(4);
});
test("should return correct min", () => {
expect(object.min).toEqual(1);
});
// other tests here
Check out the github repository of the included snippets in here for a complete picture of the tests.
The concepts and points above are the very basics of TDD using Jest. There are a lot more to learn, from more advanced matchers, mocking, testing asynchronous parts of the code, and others. I am still to learn them and that is for another dev post 😆.
Cheers to continued learning! 🍷
20