Workshop CWC Testing

I don't have time to write tests because I am too busy 

Confidence to change code: well-written tests allow you to refactor code with confidence that you’re not breaking anything, and without wasting time updating the tests.

 

Documentation: tests explain how code works and what’s the expected behavior. Tests, in comparison to any written documentation, are always up to date.

 

Bugs and regression prevention: by adding test cases for every bug, found in your app, you can be sure that these bugs will never come back. Writing tests will improve your understanding of the code and the requirements, you’ll critically look at your code and find issues that you’d miss otherwise.

Why automate testing

Static analysis catches syntax errors, bad practices and incorrect use of APIs:  — Code formatters, like Prettier;  — Linters, like ESLint;  — Type checkers, like TypeScript and Flow.

Unit tests verify that tricky algorithms work correctly. Tools: Jest.

Integration tests give you confidence that all features of your app work as expected. Tools: Jest and Enzyme or react-testing-library.

End-to-end tests make sure that your app work as a whole: the frontend and the backend and the database and everything else. Tools: Cypress.

The different tests

The different tests

Unit Tests

Jest

Jest, Jest DOM, React Testing Library

Integration Tests

UI

Tests

Cypress, TestCafe

Unit tests

Best practices

1. Consistent 

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

Multiple runs of the test should consistently return true or consistently return false, provided no changes

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

Only two possible results : PASS or FAIL
No partially successful tests

No side effects between tests


Isolation of tests

  • Different execution order must yield same results
  • Test B should not depend on outcome of Test A (use mocks instead)

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

One test should be responsible for one scenario only
Test behavior, not methods

  • One method, multiple behaviors -> multiple tests
  • One behavior, multiple methods -> one test

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

Unit test must be easy to read and understand

  • variable, method and class names must be self descriptive
  • no conditional logic
  • no loops

Name tests to represent PASS conditions:

  • canMakeReservation
  • totalBillEqualsSumOfMenuItemPrices

Test should have no uncertainty:

  • All inputs should be known
  • Method behavior should be predictable
  • Expected output should be strictly defined
  • Split in to two tests rather than using "if" or "switch"

Test should not contain "while", "for" or "foreach" loops

  • If test logic has to be repeated, it probably means the test is too complicated
  • Call method multiple times rather than looping inside of method

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

Indicate expected exception
Catch only the expected type of exception
Fail test if expected exception is not caught
Let other exceptions go uncaught

By reading the assertion message, one should know why the test failed and what to do
Include business logic information in the assertion message
Good assertion messages:

  • Improve code documentation
  • Inform developers about the problem if the test fails

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

Unit tests

Best practices

1. Consistent

2. Atomic

3. Single Responsibility

4. Self-descriptive

5. No conditional logic or loops

6. No exception handling

7. Informative Assertion messages

8. No test logic in production code

  • Separate Unit tests ans Production code in separate projects
  • Do not create Methods or properties used only by unit tests.
  • Use Dependency Injection or Mocks to isolate Production code

Pattern Arrange/Act/Assert

(aka AAA)

1. Arrange—Perform the setup and initialization required for the test.

2. Act—Take action(s) required for the test

3. Assert—Verify the outcome(s) of the test.

AAA is a pattern for organizing tests. It breaks tests down into three clear and distinct steps:

Pattern Arrange/Act/Assert

(aka AAA)

it('should do something', function() {
  //arrange...
  var dummyData = { foo: 'bar' };
  var expected = 'the result we want';

  //act...
  var result = functionUnderTest(dummyData);

  //assert...
  expect(result).to.equal(expected);
});

Pattern Arrange/Act/Assert

(aka AAA)

Benefits :

  • Creates a clear separation between a test’s setup, operations and results

  • Makes the code easier to read and understand

  • Enforces a certain degree of discipline when writing tests

  • assertSame

  • assertEquals

  • assertTrue

  • assertFalse

  • assertCount

  • assertInstanceOf

  • ...

Unit tests

Assertions

Only one assertion per tests if possible but not an absolute rule

 

Multiple assertions in one test method can reveal sometimes a wrong granularity of tests

Unit tests

Assertions

Unit tests

Assertions

// Arrange, Act and...

$this->assertTrue(
  $container->hasDefinition('logger')
);
$this->assertSame(
  'FileLogger',
  $container->getDefinition('logger')->getClass()
);
$this->assertEquals(
  array('dev.log'),
  $container->getDefinition('logger')->getArguments()
);

Unit tests

Group Assertions

public function testFileLoggerService()
{
    $this->containerHasServiceWithClassAndArguments(
        'logger',
        'FileLogger'
        array('dev.log')
    );
}

private function containerHasServiceWithClassAndArguments(
    ContainerInterface $container,
    $serviceId,
    $expectedClass,
    array $expectedArguments
) {
    $this->assertTrue(
      $container->hasDefinition($serviceId)
    );
    $this->assertSame(
      $expectedClass,
      $container->getDefinition($serviceId)->getClass()
    );
    $this->assertEquals(
      $expectedArguments,
      $container->getDefinition($serviceId)->getArguments()
    );
}

Unit tests

Describe what you are doing

public function testSizeOfTheCollectionReflectsNewElements()
{
    $collection = $this->emptyCollection();

    $collection->add($this->anyElement());

    $this->collectionHasOneElement($collection);
}

private function emptyCollection()
{
    return new Collection();
}

private function anyElement()
{
    return 'any element';
}

private function collectionHasOneElement($collection)
{
    $this->assertSame(1, count($collection);
}

Mocking

Definition

A object that you want to test, the subject under test (SUT), may have dependencies (aka collaborator) on other complex objects.

 

To isolate the behavior of the object you want to test you replace the other objects by mocks that simulate the behavior of the real objects.

Mocking

Prevent unreadable long-list

of set-up code for all the mocks

$object = ...;

$validationViolationList = $this->getMock('ValidationViolationList');
$validationViolationList
    ->expects($this->any())
    ->method('count')
    ->will($this->returnValue(1));

$validator = $this->getMock('Validator');
$validator
    ->expects($this->once())
    ->method('validate')
    ->with($object)
    ->will($this->returnValue($validationViolationList);

Mocking

Mocks can badly influence the stability of your test suite

Prevent your test to fail whenever you make structural changes to the SUT by not being too rigid about:

  • the number of times a method is called
  • the order in which methods are called
  • the arguments used when calling a method

Build some flexibility into your test doubles.

Mocking

Test Doubles

Gerard Meszaro worked at capturing patterns for using the various Xunit frameworks.

The generic term he uses is a Test Double. Test Double is a generic term for any case where you replace a production object for testing purposes.

Mocking

Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.

// Logger is an interface
$logger = $this->getMock('Logger');

Mocking

Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production.

Mocking

$urlsByRoute = array(
    'index' => '/',
    'about_me' => '/about-me'
);

$urlGenerator = $this->getMock('UrlGenerator');
$urlGenerator
    ->expects($this->any())
    ->method('generate')
    ->will($this->returnCallback(
        function ($routeName) use ($urlsByRoute) {
            if (isset($urlsByRoute[$routeName])) {
                return $urlsByRoute[$routeName];
            }

            throw new UnknownRouteException();
        }
    ));

Mocking

$urlGenerator = $this->getMock('UrlGenerator');
$urlGenerator
    ->expects($this->any())
    ->method('match')
    ->will($this->returnArgument(0));

Mocking

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

$logger = $this->getMock('Logger');
$logger
    ->expects($this->any())
    ->method('getLevel')
    ->will($this->returnValue(Logger::INFO));

Mocking

Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.

Mocking

$logger = $this->getMock('Logger');
$logger
    ->expects($spy = $this->any())
    ->method('debug');

$invocations = $spy->getInvocations();

$this->assertSame(1, count($invocations));

$firstInvocation = $invocations[0];
$this->assertEquals(
  'An error occurred',
  $firstInvocation->parameters[0]
);

Mocking

Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.

Mocking

$logger = $this->getMock('Logger');

$logger
    ->expects($this->at(0))
    ->method('debug')
    ->with('A debug message');

$logger
    ->expects($this->at(1))
    ->method('info')
    ->with('An info message');

Mocking

Manually create your test double classes

class LoggerDummy implements Logger
{
    public function debug()
    {
        return null; // not even necessary here
    }

    ...
}

class InfoLevelLoggerStub implements Logger
{
    ...

    public function getLevel()
    {
        return self::INFO;
    }
}

Mocking

Manually create your test double classes

class PredefinedUrlsUrlGeneratorFake implements UrlGenerator
{
    private $urlsByRoute;

    public function __construct(array $urlsByRoute)
    {
        $this->urlsByRoute = $urlsByRoute;
    }

    public function generate($routeName)
    {
        if (isset($this->urlsByRoute[$routeName])) {
            return $this->urlsByRoute[$routeName];
        }

        throw new UnknownRouteException();
    }
}

Mocking

At least use factory methods

public function testSomething()
{
    $logger = $this->loggerDummy();
}

private function loggerDummy()
{
    return $this->getMockBuilder('Logger')
        ->setMethods(array('debug', 'info', ...))
        ->getMock();
}

Mocking

  • Safer refactoring

  • Smaller, tighter, decoupled code

  • Documentation of requirements

  • Continuous integration

  • Value of tests increase over time

Unit tests

Benefits

After/during coding

  • Focus on code

  • Thinking about algorithm

  • More refactoring

  • Easier initially

Unit tests

When to write tests?

Before coding (TDD, BDD)

  • Focus on requirements

  • Thinking about how code will be consumed

  • Stop coding when requirements met

  • Harder initially (needs some practice)

Unit tests

When to write tests?

TDD Benefits :

  1. Writing the tests first requires you to really consider what do you want from the code.
  2. You receive fast feedback.

  3. TDD creates a detailed specification.

  4. TDD reduces time spent on rework.

  5. You spend less time in the debugger.

  6. You are able to identify the errors and problems quickly.

  7. TDD tells you whether your last change (or refactoring) broke previously working code.

  8. TDD forces the radical simplification of the code. You will only write code in response to the requirements of the tests.

TDD Benefits :

9. You're forced to write small classes focused on one thing.

10. TDD creates SOLID code that is maintainable, flexible, and easily extensible.

11. The resulting unit tests are simple and act as documentation for the code. Since TDD use cases are written as tests, other programmers can view the tests as usage examples of how the code is intended to work.

12. The development time to market is shorter.

13. The programmer’s productivity is increased.

14. Quality is improved.

15. Bugs are reduced.

16. Development costs are cut in long term.

Integration Tests

The point of integration testing, as the name suggests, is to test whether many separately developed modules work together as expected. (Martin Fowler)

Integration Tests

Jest

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.

Jest

zero config

Jest aims to work out of the box, config free, on most JavaScript projects.

 

snapshots

Make tests which keep track of large objects with ease. Snapshots live either alongside your tests, or embedded inline.

 

Jest

isolated

Tests are parallelized by running them in their own processes to maximize performance.

 

great api

From it to expect - Jest has the entire toolkit in one place. Well documented, well maintained, well good.

 

Jest

Benefits over other test runners

  • Very fast.
  • Interactive watch mode that only runs tests which are relevant to your changes.
  • Helpful failure messages.
  • Simple configuration, or even zero configuration.
  • Mocks and spies.
  • Coverage reports.
  • Rich matchers API.
 

Jest

Matchers

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

The simplest way to test a value is with exact equality.

Jest

Truthiness Matchers

  • 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

Jest

Truthiness Matchers

test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

Jest

Number Matchers

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

Jest

String Matchers

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

Jest

Array and iterable Matchers

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'beer',
];

test('the shopping list has beer on it', () => {
  expect(shoppingList).toContain('beer');
  expect(new Set(shoppingList)).toContain('beer');
});

Jest

Exception Matchers

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(Error);

  // You can also use the exact error message or a regexp
  expect(compileAndroidCode).toThrow('you are using the wrong JDK');
  expect(compileAndroidCode).toThrow(/JDK/);
});

Jest

Custom Matchers

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be within range ${floor} - ${ceiling}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be within range ${floor} - ${ceiling}`,
        pass: false,
      };
    }
  },
});

Jest

Custom Matchers

test('numeric ranges', () => {
  expect(100).toBeWithinRange(90, 110);
  expect(101).not.toBeWithinRange(0, 100);
  
  expect({apples: 6, bananas: 3}).toEqual({
    apples: expect.toBeWithinRange(1, 10),
    bananas: expect.not.toBeWithinRange(11, 20),
  });
});

Jest

Asynchronous code testing

// Don't do this!
test('the data is peanut butter', () => {
  function callback(data) {
    expect(data).toBe('peanut butter');
  }

  fetchData(callback);
});

Jest

Asynchronous code testing

test('the data is peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }

  fetchData(callback);
});

Jest will wait until the done callback is called before finishing the test.

If done() is never called, the test will fail

Jest

Asynchronous code testing

test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

Just return a promise from your test, and Jest will wait for that promise to resolve. If the promise is rejected, the test will automatically fail.

Jest

Setup and Teardown

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

Repeating Setup For Many Tests

Jest

Setup and Teardown

beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

One-Time Setup

Jest

Mock Functions

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);

Jest

Mock Return Values

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock
  .mockReturnValueOnce(10)
  .mockReturnValueOnce('x')
  .mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

Jest

Mocking Modules

// users.js
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

Jest

Mocking Modules

// users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});

Jest

Mock Implementations

const returnsTrue = jest.fn(() => true);

console.log(returnsTrue());
// true;

Jest

Mock Implementations

// foo.js
module.exports = function() {
  // some implementation;
};

// test.js
jest.mock('../foo');
// this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

Jest

Mock Implementations

const myMockFn = jest
  .fn(() => 'default')
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

Jest

Custom Matchers

// The mock function was called at least once
expect(mockFunc).toBeCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toBeCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).lastCalledWith(arg1, arg2);

Jest

Custom Matchers

// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);

// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
  arg1,
  arg2,
]);

Jest Dom

Custom Jest matchers to test the state of the DOM

 

These will make your tests more declarative, clear to read and to maintain.

 

@testing-library/jest-dom

Jest Dom

toBeDisabled

<button data-testid="button" type="submit" disabled>submit</button>
<fieldset disabled><input type="text" data-testid="input" /></fieldset>
<a href="..." disabled>link</a>

// Using document.querySelector

expect(document.querySelector('[data-testid="button"]')).toBeDisabled()
expect(document.querySelector('[data-testid="input"]')).toBeDisabled()
expect(document.querySelector('a')).not.toBeDisabled()

// Using DOM Testing Library

expect(getByTestId(container, 'button')).toBeDisabled()
expect(getByTestId(container, 'input')).toBeDisabled()
expect(getByText(container, 'link')).not.toBeDisabled()

This allows you to check whether an element is disabled from the user's perspective.

Jest Dom

toBeInvalid

This allows you to check if a form element, or the entire form, is currently invalid.

 

An input, select, textarea, or form element is invalid if it has an aria-invalid attribute with no value or a value of "true", or if the result of checkValidity() is false.

Jest Dom

toBeInvalid

<input data-testid="no-aria-invalid" />
<input data-testid="aria-invalid" aria-invalid />
<input data-testid="aria-invalid-value" aria-invalid="true" />
<input data-testid="aria-invalid-false" aria-invalid="false" />

<form data-testid="valid-form">
  <input />
</form>

<form data-testid="invalid-form">
  <input required />
</form>

// Using document.querySelector

expect(queryByTestId('no-aria-invalid')).not.toBeInvalid()
expect(queryByTestId('aria-invalid')).toBeInvalid()
expect(queryByTestId('aria-invalid-value')).toBeInvalid()
expect(queryByTestId('aria-invalid-false')).not.toBeInvalid()

expect(queryByTestId('valid-form')).not.toBeInvalid()
expect(queryByTestId('invalid-form')).toBeInvalid()

// Using DOM Testing Library

expect(getByTestId(container, 'no-aria-invalid')).not.toBeInvalid()
expect(getByTestId(container, 'aria-invalid')).toBeInvalid()
expect(getByTestId(container, 'aria-invalid-value')).toBeInvalid()
expect(getByTestId(container, 'aria-invalid-false')).not.toBeInvalid()

expect(getByTestId(container, 'valid-form')).not.toBeInvalid()
expect(getByTestId(container, 'invalid-form')).toBeInvalid()

Jest Dom

Other Matchers

  • toBeEnabled
  • toBeEmpty
  • toBeInTheDocument
  • toBeRequired
  • toBeValid
  • toBeVisible
  • toContainElement
  • toContainHTML
  • toHaveAttribute
  • toHaveClass
  • toHaveFocus
  • toHaveFormValues
  • toHaveStyle
  • toHaveTextContent
  • toHaveValue

Mocking

HTTP server mocking

Nock over Moxios

 

Nock is not related to a specific HTTP client

Nock works by overriding Node's http.request function.

Mocking

HTTP server mocking

const nock = require('nock')

const scope = nock('https://api.github.com')
  .get('/repos/atom/atom/license')
  .reply(200, {
    license: {
      key: 'mit',
      name: 'MIT License',
      spdx_id: 'MIT',
      url: 'https://api.github.com/licenses/mit',
      node_id: 'MDc6TGljZW5zZTEz',
    },
  })

Code Coverage

you can't go into production with less than 87% coverage

Code Coverage

  • Metric uses to determine code covered by unit tests
  • Useful to find untested code
  • Do not guarantee test quality and use cases coverage

 

We can have 100% code coverage without any assertions

Code Coverage

yarn test --coverage src/modules/Wallet

Code Coverage

Code Coverage

Problems with stribing to high test coverage

  • High test coverage gives you a false sense of security. “Covered code” means the code was executed during a test run but it doesn’t mean that tests were actually verifying what this code does. With less than 100% test coverage you can be sure you’re not testing some code, but even with 100% coverage, you can’t be sure you’re testing everything.
  • Some features are really hard to test, like file upload in a browser or drag’n’drop. You start mocking or accessing component internals, so your tests no longer resemble how your users use your app, and hard to maintain. Eventually, you start spending more time on writing less useful tests — so-called problem of diminishing returns.
  • Some features does not really need to be tested

Enzyme

Enzyme is a JavaScript Testing utility for React that makes it easier to test your React Components' output. You can also manipulate, traverse, and in some ways simulate runtime given the output.

Enzyme

Usage

  • Shallow rendering does call React lifecycle methods such as componentDidMount and componentDidUpdate

  • Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs

  • Static Rendered Markup allow to generate HTML from your React tree, and analyze the resulting HTML structure

Enzyme

disclaimer

Enzyme

With great power comes great responsibility

Enzyme

Risks

  • False positive

  • Break test when implementation changes (state naming for instance)

 

 

Guiding principle

The more your tests resemble the way your software is used, the more confidence they can give you.

 

Testing Library

Simple and complete testing utilities that encourage good testing practices

Testing Library

  • Write maintainable tests

  • Develop with confidence

  • Accessible by Default

Testing Library

The problem

  • You want tests for your UI that avoid including implementation details and rather focus on making your tests give you the confidence for which they are intended.

  • You want your tests to be maintainable so refactors (changes to implementation but not functionality) don't break your tests and slow you and your team down.

Testing Library

The solution

The Testing Library family of libraries is a very light-weight solution for testing without all the implementation details.

 

The main utilities it provides involve querying for nodes similarly to how users would find them. In this way, testing-library helps ensure your tests give you confidence in your UI code.

React Testing Library

Simple and complete React DOM testing utilities that encourage good testing practices.

React Testing Library

Test behavior over implementation

  • Small API, uses the DOM

  • Components tested like they’re used (Privates stay private!)

  • Encourages/Enforces accessible applications (getByLabelText, getByPlaceholderText, getByText, getByAltText)

  • Escape-hatches for everything else (getByTestId, container.querySelector (DOM APIs))

React Testing Library

DOM Queries

No Match 1 Match 1+ Match Await?
getBy throw return throw No
findBy throw return throw Yes
queryBy null return throw No
getAllBy throw array array No
findAllBy throw array array Yes
queryAllBy [] array array No

React Testing Library

DOM Queries

  • ByLabelText find by label or aria-label text content
  • ByPlaceholderText find by input placeholder value
  • ByText find by element text content
  • ByDisplayValue find by form element current value
  • ByAltText find by img alt attribute
  • ByTitle find by title attribute or svg title tag
  • ByRole find by aria role
  • ByTestId find by data-testid attribute

React Testing Library

Async Utilities

Several utilities are provided for dealing with asynchronous code. These can be useful to wait for an element to appear or disappear in response to an action.

 

  • wait
  • waitForElement
  • waitForDomChange
  • waitForElementToBeRemoved

React Testing Library

Example

import React, { useState } from 'react'
import axios from 'axios'

export default function Fetch({ url }) {
  const [greeting, setGreeting] = useState('')
  const [buttonClicked, setButtonClicked] = useState(false)

  const fetchGreeting = async () => {
    const response = await axios.get(url)
    const data = response.data
    const { greeting } = data
    setGreeting(greeting)
    setButtonClicked(true)
  }

  const buttonText = buttonClicked ? 'Ok' : 'Load Greeting'

  return (
    <div>
      <button
        onClick={fetchGreeting}
        data-testid="ok-button"
        disabled={buttonClicked}
      >
        {buttonText}
      </button>
      {greeting ? <h1 data-testid="greeting-text">{greeting}</h1> : null}
    </div>
  )
}

React Testing Library

Debugging

Sometimes you want to check the rendered React tree, use the debug() method: 

const { debug } = render(<p>Hello Jest!</p>);
debug();
// -> <p>Hello Jest!</p>
debug(getByText(/expand/i));

You can also print an element:

React Testing Library

Example

import React from 'react'
import { render, fireEvent, waitForElement } from '@testing-library/react'
import axiosMock from 'axios'
import Fetch from '../fetch'

test('loads and displays greeting', async () => {
  // Arrange
  const url = '/greeting'
  const { getByText, getByTestId } = render(<Fetch url={url} />)

  axiosMock.get.mockResolvedValueOnce({
    data: { greeting: 'hello there' },
  })

  // Act
  fireEvent.click(getByText('Load Greeting'))

  const greetingTextNode = await waitForElement(() =>
    getByTestId('greeting-text')
  )

  // Assert
  expect(axiosMock.get).toHaveBeenCalledTimes(1)
  expect(axiosMock.get).toHaveBeenCalledWith(url)
  expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
  expect(getByTestId('ok-button')).toHaveAttribute('disabled')
})

React Hooks testing

When to use this library?

  1. You're writing a library with one or more custom hooks that are not directly tied a component

  2. You have a complex hook that is difficult to test through component interactions

React Hooks testing

When not to use this library

  1. Your hook is defined along side a component and is only used there

  2. Your hook is easy to test by just testing the components using it

React Hooks testing

import { useState, useCallback } from 'react'

function useCounter() {
  const [count, setCount] = useState(0)

  const increment = useCallback(() => setCount((x) => x + 1), [])

  return { count, increment }
}

export default useCounter

React Hooks testing

import { renderHook, act } from '@testing-library/react-hooks'
import useCounter from './useCounter'

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter())

  act(() => {
    result.current.increment()
  })

  expect(result.current.count).toBe(1)
})

Test isolation

  • Improve the reliability of the tests

  • Simplify the code

  • Increase the confidence on your tests

import React from 'react'

function Counter(props) {
  const initialProps = React.useRef(props).current
  const {initialCount = 0, maxClicks = 3} = props
  
  const [count, setCount] = React.useState(initialCount)
  const tooMany = count >= maxClicks
  
  const handleReset = () => setCount(initialProps.initialCount)
  const handleClick = () => setCount(currentCount => currentCount + 1)
  
  return (
    <div>
      <button onClick={handleClick} disabled={tooMany}>
        Count: {count}
      </button>
      {tooMany ? <button onClick={handleReset}>reset</button> : null}
    </div>
  )
}

export {Counter}
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import {Counter} from '../counter'

const {getByText} = render(<Counter maxClicks={4} initialCount={3} />)
const counterButton = getByText(/^count/i)

test('the counter is initialized to the initialCount', () => {
  expect(counterButton).toHaveTextContent('3')
})

test('when clicked, the counter increments the click', () => {
  fireEvent.click(counterButton)
  expect(counterButton).toHaveTextContent('4')
})

test(`the counter button is disabled when it's hit the maxClicks`, () => {
  fireEvent.click(counterButton)
  expect(counterButton).toHaveAttribute('disabled')
})

test(`the counter button does not increment the count when clicked when it's hit the maxClicks`, () => {
  expect(counterButton).toHaveTextContent('4')
})

test(`the reset button has been rendered and resets the count when it's hit the maxClicks`, () => {
  fireEvent.click(getByText(/reset/i))
  expect(counterButton).toHaveTextContent('3')
})

Test isolation

What's going on if we remove a test?

So let's try something else and see how that changes things

Test isolation

import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import {Counter} from '../counter'

let getByText, counterButton

beforeEach(() => {
  const utils = render(<Counter maxClicks={4} initialCount={3} />)
  getByText = utils.getByText
  counterButton = utils.getByText(/^count/i)
})

test('the counter is initialized to the initialCount', () => {
  expect(counterButton).toHaveTextContent(/3)
})

test('when clicked, the counter increments the click', () => {
  fireEvent.click(counterButton)
  expect(counterButton).toHaveTextContent(/4)
})

test(`the counter button is disabled when it's hit the maxClicks`, () => {
  fireEvent.click(counterButton)
  expect(counterButton).toHaveAttribute('disabled')
})

test(`the counter button does not increment the count when clicked when it's hit the maxClicks`, () => {
  fireEvent.click(counterButton)
  fireEvent.click(counterButton)
  expect(counterButton).toHaveTextContent(/4)
})

test(`the reset button has been rendered and resets the count when it's hit the maxClicks`, () => {
  fireEvent.click(counterButton)
  fireEvent.click(getByText(/reset/i))
  expect(counterButton).toHaveTextContent(/3)
})

It's better but that lead to tests that are harder to understand.

Test isolation

import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import {Counter} from '../counter'

function renderCounter(props) {
  const utils = render(<Counter maxClicks={4} initialCount={3} {...props} />)
  const counterButton = utils.getByText(/^count/i)
  return {...utils, counterButton}
}
test('the counter is initialized to the initialCount', () => {
  const {counterButton} = renderCounter()
  expect(counterButton).toHaveTextContent(/3)
})
  
test('when clicked, the counter increments the click', () => {
  const {counterButton} = renderCounter()
  fireEvent.click(counterButton)
  expect(counterButton).toHaveTextContent(/4)
})
  
test(`the counter button is disabled when it's hit the maxClicks`, () => {
  const {counterButton} = renderCounter({
    maxClicks: 4,
    initialCount: 4,
  })
  expect(counterButton).toHaveAttribute('disabled')
})
  
test(`the counter button does not increment the count when clicked when it's hit the maxClicks`, () => {
  const {counterButton} = renderCounter({
    maxClicks: 4,
    initialCount: 4,
  })
  fireEvent.click(counterButton)
  expect(counterButton).toHaveTextContent(/4)
})
  
test(`the reset button has been rendered and resets the count when it's hit the maxClicks`, () => {
  const {getByText, counterButton} = renderCounter()
  fireEvent.click(counterButton)
  fireEvent.click(getByText(/reset/i))
  expect(counterButton).toHaveTextContent(/3)
})

Test isolation

Cleanup now enabled by default in React Testing Library

E2E tests using Cypress

Before Cypress

  • Choose a framework (Mocha, Jasmine, QUnit, Karma)
  • Choose an assertion library (Chai, Expect.js)

  • Choose a Selenium wrapper (Protractor, Nightwatch, Webdriver)

  • Add additional libraries (Sinon, TestDouble)

E2E tests using Cypress

All-in-one testing framework

Assertion library, with mocking and stubbing, all without Selenium

E2E tests using Cypress

describe(‘wallet’, () => {
 before(() => {
   cy.login();
 });
 
 it(‘Visits the wallet page’, () => {
   cy.server();
   
   cy.route(‘GET’, ‘product-referentials*’, ‘fixture:product-referentials.json’);
   cy.route(‘GET’, ‘transactions*’, ‘fixture:transactions.json’);
   cy.route(‘GET’, ‘cards*’, ‘fixture:cards.json’);
   
   cy.visit(‘/wallet’);
 });
});

E2E tests using Cypress

A test runner built for humans.

Any questions?

CWC Testing Workshop

By Nicolas Eeckeloo

CWC Testing Workshop

  • 671