Basics of Javascript Test Driven Development (TDD) with Jest

[JS#4 WIL 🤔 Post]

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.

  1. Why does most devs hate tests? Because they are slow and fragile and expensive (time).
  2. It is perfectly valid to delete some tests.
  3. 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 🤔]).
  4. Do not test private methods. But break this rule if it saves money during development.
  5. 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.
  6. Trust collaborators that they will do the right thing. Insist on simplicity.
  7. 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.

📌 Grid of Test Rules

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

📌 Advantages of TDD

  1. Reduces bugs that may be introduced when adding new features or modifying existing features
  2. Builds a safety net against changes of other programmers that may affect a specific part of the code
  3. Reduces the cost of change by ensuring that the code will still work with the new changes
  4. Reduces the need for manual (monkey) checking by testers and developers
  5. Improves confidence in code
  6. Reduces the fear of breaking changes during refactors

📌 Getting Started with Jest

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.

📌 Three Modes of TDD

  1. Obvious implementation. You write the test with the implementation since you know how to implement the method to test.
  2. 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."
  3. 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.

📌 Using Jest Matchers

Common Matchers

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.

Truthiness

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 only null
  • toBeUndefined matches only undefined
  • toBeDefined is the opposite of toBeUndefined
  • toBeTruthy matches anything that an if statement treats as true
  • toBeFalsy matches anything that an if statement treats as false
Numbers

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

Strings can be checked against regular expressions using toMatch.

Arrays and Iterables

toContain can be used to check if a particular item can be found in an array or iterable.

Exceptions

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.

📌 Jest Testing Practice

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, and multiply. 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, and length.
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! 🍷

[REFERENCES]

20