💻
Building and hosting a WebApp
  • Getting started
  • Project setup
    • Requirements
    • Files organisation
    • Lerna
    • Linter
    • Prettier
    • GitHook
    • Testing
    • Conclusion
  • Backend
    • Files organisation
    • Environment config
    • Express API
    • Security
    • Database
    • GraphQL
    • User authentication
    • Conclusion
  • Frontend
    • Create React App
    • Files organisation
    • Styles
    • Apollo Hooks
    • Form management
    • User authentication
    • Writing tests
    • Types generation
    • Conclusion
  • DevOps
    • CI/CD
    • AWS
      • Managing secrets
      • Pricing
      • RDS
      • S3
      • Route53
      • CloudFront
      • Serverless
      • Security
      • CloudFormation
    • Conclusion
  • 🚧Stripe payment
  • 🚧File upload
Powered by GitBook
On this page
  • Setting up Jest with TypeScript
  • Writing tests
  • Mocking dependencies is hard
  • Dependencies injection
  • Containers file
  • Code coverage
  • Why do we even write test?
  • To test or not to test

Was this helpful?

  1. Project setup

Testing

Setting up Jest with TypeScript

To keep thing clear and simple, we will move all the code related to the app to a new src folder. For now, we just have index.ts, but the number of files will quickly grow.

$ cd packages/api
$ mkdir src
$ mv index.ts src

Create jest.config.js in packages/api with the following content:

jest.config.js
module.exports = {
  preset: 'ts-jest',
  rootDir: 'src',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  testEnvironment: 'node',
  moduleFileExtensions: ['js', 'ts', 'tsx'],
  moduleDirectories: ['node_modules'],
  coverageReporters: ['html'],
  setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
  globals: {
    'ts-jest': {
      tsConfig: 'tsconfig.json',
    },
  },
};

Create setupTests.ts and leave it empty. Anything in this file is run before the tests. We will need it later.

Now is also a good time to create our tsconfig.json file in packages/api.

$ cd packages/api
$ tsc --init
$ yarn add esnext

Replace the content with the following:

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "sourceMap": true,
    "declaration": true,
    "strictNullChecks": true,
    "noUnusedLocals": false,
    "allowUnusedLabels": false,
    "noUnusedParameters": false,
    "pretty": true,
    "skipLibCheck": true,
    "lib": ["es2015", "esnext.asynciterable"]
  },
  "sourceMap": true,
  "outDir": ".build",
  "moduleResolution": "node",
  "rootDir": "./src"
}

Writing tests

There are 2 things that are notoriously difficult when unit testing:

  1. Mocking imports

  2. Testing asynchronous code

Let's write a function to fetch all the emojis available on GitHub.

First, install the required dependencies

$ cd packages/api
$ yarn add node-fetch @types/node-fetch
$ yarn add jest @types/jest ts-jest -D

Fetch the emojis:

index.ts
import fetch from "node-fetch";

const getEmoji = async (emoji: string): Promise<string> => {
    const response = await fetch("https://api.github.com/emojis");
    const data = await response.json();
    return data[emoji];
};

export { getEmoji };

Now we have a problem, how do we mock the request to GitHub? We don't want to send the request each time we run the test.

Mocking dependencies is hard

A lot of people struggle with setting up all the mocking required to isolate a function. They would end up writing a large amount of code to setup the test, but little to test the actual function.

Dependencies injection

Injecting dependencies allows us to arbitrarily set a dependency at any time. This is perfect for unit testing. We can also leverage some TypeScript functionalities around classes.

Let's get started:

$ yarn add typedi

Create a new file Emoji.tswith a class Emoji

Emoji.ts
import fetch from "node-fetch";
import { Container } from "typedi";

class Emoji {
  private readonly _fetch: typeof fetch = Container.get("fetch");
  async get(emoji: string): Promise<string> {
    const response = await this._fetch("https://api.github.com/emojis");
    const data = await response.json();
    return data[emoji];
  }
}

export { Emoji };

By using private readonly _fetch: typeof fetch = Container.get("fetch"); we can now set fetch to anything we'd like.

Emoji.spec.ts
import { Emoji } from "./Emoji";
import { Container } from "typedi";

describe("fetchRepositories function", () => {
  it("should fetch and return emoji passed as a param", async () => {
    // Arrange: setup everything we need
    const computer = "mocked url";
    const data = { computer };
    const json = jest.fn().mockResolvedValue(data);
    const fetch = jest.fn(() => ({ json }));
    Container.set("fetch", fetch);

    const emoji = new Emoji();

    // Act: execute the code we want to test
    const result = await emoji.get("computer");

    // Assert: check if it worked
    expect(result).toStrictEqual(computer);
  });
});

Structure your tests with 3 well-separated sections: Arrange, Act & Assert.

Containers file

To avoid future mistake, we will create a containers.ts file to list all the containers available and their type.

containers.ts
import fetch from "node-fetch";
import { Container } from "typedi";

enum ContainerNames {
    fetch = 'fetch'
}

export const setFetch = (fn: typeof fetch) => Container.set(ContainerNames.fetch, fn)
export const getFetch = (): typeof fetch => Container.get(ContainerNames.fetch)

And we can now use it in our Emoji.ts class

Emoji.ts
import { getFetch } from './containers'

class Emoji {
    private readonly _fetch = getFetch()
    async get(emoji: string): Promise<string> {
        const response = await this._fetch("https://api.github.com/emojis");
        const data = await response.json();
        return data[emoji];
    }
}

export { Emoji };

That's it. We can now mock anything, anywhere in our code.

Code coverage

If you run jest index.spec.ts --coverage you will get a new folder src/coverage

Open coverage/index.html in your browser.

Emoji.ts has 100% code coverage. Let's add a if statement and see what happens.

Emoji.ts
import { getFetch } from './containers'

class Emoji {
    private readonly _fetch = getFetch()
    async get(emoji: string): Promise<string> {
        const response = await this._fetch("https://api.github.com/emojis");
        const data = await response.json();
        if (!data[emoji]) {
            // this is not covered by any test
            return 'emoji not found';
        }
        return data[emoji];
    }
}

export { Emoji };

Run jest index.spec.ts --coverage again

There are no tests going though this if statement.

This is a fantastic tool to see how much of your code is tested.

Add coverage to .gitignore

Why do we even write test?

Some people struggle to understand why unit tests are useful. So why should we write test?

It is documentation

If anything, by reading the list of "it should..." someone can quickly get an understanding of what this class is about. This someone could be you in a few months.

If it can break, it will break

Sometimes we get distracted, we make mistakes and we break things. That's why it's important to have a few basic tests to automatically check if everything still does what it's supposed to do.

It can be part of the debugging process

Instead of restarting the server, sending a request and looking at the result, you can write a test. You can setup the scenario, call the function and assert the result. It speeds up the feedback loop.

Refactoring becomes easier

If you spot something you don't like and want to improve, go ahead. Assuming there's good coverage, if something breaks, tests will catch it.

To test or not to test

So people believe they must achieve 100% code coverage at all cost. While tests are great most of the time, writing tests for the sake of it is counter-productive. Test what makes sense. If something breaks, write a test so it doesn't happen again. With experience, you'll develop a sense for what needs to be tested and what doesn't.

The last thing you want to do is making your codebase more complex to test unimportant parts.

Test and/or mock when it makes sense.

PreviousGitHookNextConclusion

Last updated 5 years ago

Was this helpful?

branch available on GitHub

testing