Free React Course
For Beginners

L11 - Testing Your Code

React For Beginners by Itera

Key Points

  • Why test
  • Testing pyramid
  • What should be tested
  • Testing with Jest

Why test

Find mistakes earlier

People do mistakes, that's normal.

Tests help you identify mistakes in earlier stages which makes fix much more easy.

Prevent from new mistakes

Code is a subject to change. When changing, it's easy to forget update something related or break something else.

Tests prevent accidental bugs.

Code is cleaner

Writing tests, you will eventually use the code. If the code is hard to test - this is a spot that code will be hard to use.

Tests highlight bad code smells.

Code is better

Writing tests, you are thinking about edge cases - errors, fails and other crap happening in a real life.

Tests make you think more about your code.

Tests are crucial for the business because of Time to Market

Business want to deliver features as fast as possible, better - yesterday. Manual testing is slow. No testing is risky.

Tests find bugs fast. Everybody happy.

Testing pyramid

pyramid of testing

Unit tests - the most popular and the most fastest tests

Unit tests exists to test a single unit behavior in full isolation

Code for a unit test

const calculate = (income, percentage) => income * percentage 

Single component also a good candidate for the unit test

const Greeter = ({name})=> 

hello {name}

Integration tests

Integration tests exists to check, how does your components interact with each other.

Code for a integration testing

import { calculatePercentage } from './percentage-calculator';
import { calculateFullPrice } from './price-calculator';

export function someComplexCalculations() {
	const percentage = calculatePercentage(5000, 25);
  return calculateFullPrice(1000_000, percentage); 
}
  

Forms, complex blocks also a good candidates for the integration tests

UI testing

UI tests - usually tests some workflow, but in isolation from the real data.

Cypress, Playwright are good tools to help you with

End to End tests - most complex and time consuming testing

E2e tests testing the whole business scenario. From page load to desired action. Backend, DataBase are included

E2E tests that the whole application works as expected

End to end tests are most expensive and most useful

What should be tested

Wrong statement: You should test business rules

Right statement: You should test things crucial to the business

Obvious cases to test

  • Unit tests - math, regex, important data on the screen
  • Integration testing - reactions, transaction, important behavior
  • E2e tests - most important business scenarios - charge, checkout, submit, etc

Shouldn't be tested

  • Internal realization - you should tests the expected result, - not the way it was achieved
  • Internal API and third party libraries
  • Each and every aspect of your code - test the most crucial one

100% Code coverage is a myth

Because...

const add = (a,b)=> a+b;
const sum = add(1,1);
expect(sum).toBe(2)

You missed

  • null/undefined/NaN
  • Non numbers e.g. string, objects, arrays
  • MAX_SAFE_INTEGER

Testing with Jest

Before we go - good test consists of:

  • Arrange - preparations (data, mocks)
  • Act - execution the tested code (render, click, fetch)
  • Assert - checking the results

Tests live in a separate files like App.test.tsx

Update App.tsx with the next code

const App = () => <div>Hello word</div>;
export default App;
  

Update App.test.tsx with the next code

import { render, screen } from "@testing-library/react";
import { act } from "react-dom/test-utils";
import App from "./App";

test("hello world should be rendered", () => {
  act(()=> { render(<App />); }); // act
  const greeting = screen.getByText(/hello world/i); // assert
  expect(greeting).toBeInTheDocument(); // assert
});
      

This is your first unit test

This unit tests checks existence of the text on the screen

Start tests with npm test

Failed test example

Test failed because we did a typo in the component.

That is how the tests works.

Test which never fail is a bad test

Real life example

type MoneyFormatterProps = {
  amount: number;
};

export const Money = ({ amount }: MoneyFormatterProps) => (
  <>{amount.toFixed(2)}</>
);
    

Test

import { render, screen } from "@testing-library/react";
import { act } from "react-dom/test-utils";
import { Money } from "./money.formatter";
      
test("Money should format 50 with two decimals - 50.00",()=> {
  const amount = 50; //arrange
  act(()=>{ render(<Money amount={amount} />);}); //act
  const formatted = screen.getByText("50.00"); 
  expect(formatted).toBeInTheDocument(); //assert
});

What if we pass 0?

Is 0.00 desired behavior?

This is how tests works - giving you hints!

How to test that callback was called

import { render, screen } from "@testing-library/react";
import { act } from "react-dom/test-utils";
import { Btn } from "./Btn";

it("when btn clicked, callback should be called", () => {
  const spy = jest.fn();
  act(()=> { render(<Btn onClick={spy}>Test</Btn>);})
  screen.getByText("Test").click();
  expect(spy).toHaveBeenCalled();
});
    

Hot to test classes, layout, elements?

Snapshot testing

npm i react-test-renderer @types/react-test-renderer -D
import { Btn } from "./Btn";
import renderer from "react-test-renderer";
      
it("snapshot", () => {
  const tree = renderer.create((
    <Btn onClick={() => null}>Test</Btn>)
  ).toJSON();
  expect(tree).toMatchSnapshot();
});
      
snapshottesting

Don't forget to commit your snapshots!

How to tests that everything looks as should?

Automated visual regression

Comparing pixel to pixel with master snapshot

The End

Wait, what if my component depends on other module?

  • Use inversion of control
  • Mock the module

Example

export const greeting = () => `hello: `;
import { greeting } from "./greeter";
type Props = { name: string };
export const AppGreetings = ({ name }: Props) => (
  <>{`${greeting()}${name}`}</>
);
    

Inversion of control - most preferable way

type Props = { name: string; greeter: () => void };
export const AppGreetings = ({ name, greeter }: Props) => (
  <>{`${greeter()}${name}`}</>
);
      

Use mock

jest.mock("./greeter", () => ({
  __esModule: true,
  greeting: () => "mocked",
}));

it("greetings should work", () => {
  act(() => {
    render(<AppGreetings name="testName" />);
  });
  
  expect(screen.getByText("mockedtestName"))
  .toBeInTheDocument();
});

If tests are so cool, why nobody* wrights test?

Tests slowdown new development

Obviously, to write a test, you need some time. And in this moment, the new feature you just wrote, is waiting

Usual business don't want to wait till you write a test

Test requires extra time for maintenance

Code is subject to change which means that tests are subject to update. Updating tests costs your time, and money for business

Tests are hard to write

You need to cover a lot of edge cases, you need to cover different combinations, you need to split components into the smallest parts. Laziness is also a reason.

Does this means that we are OK to skip tests?

It depends. If you are writing one-time script or a tiny obvious app - feel free to skip testing

If the app is subject to grow and requires maintenance, or it touches sensitive things like money or health - please test

Typical mistakes

Writing only happy cases

You should always consider, what if something went wrong - network, user input, etc

Writing fragile tests

Don't test the implementation - test only results that are important to the business

Multiple assertions

Ideally you should have a one assert in the test. It's not something a must but having massive assertions will lead to harder maintenance

TDD - test driven development

TDD - extreme programming paradigm when you write a test first

  • Write a test which eventually will fail - no code yet
  • Write a code that will pass the test
  • Refactor the code
  • Go to p.1

Pros

  • TDD ensures that each and every part of the code is tested
  • TDD allows you to refactor safely

The cons - TDD is very slow

Useful links

Join me

Twitter
GitHub