Skip to content

Testing

Viktor Bozhinov edited this page Jan 18, 2021 · 7 revisions

We use automated tests to ensure quality code and speed up the discovery of bugs and regressions and to ensure that we have delivered user stories.

Running tests

Before creating a pull request, you should ensure that all the tests pass locally. Additionally, if there is no Continuous Integration (CI) service running then code reviewers should also check that the tests pass for the branch they are reviewing

Unit tests

To run the unit tests, use the following command

yarn test

This will run all the tests and output a code coverage report. You should ensure that code coverage does not regress if no code coverage tool is integrated as part of the pull request process.

End-to-end (e2e) tests

To run the e2e tests, use the following command

yarn e2e

This will build a production version of the code, serve it and run the e2e tests against said served site.

If you would like to see the browser interactions yourself, run the command:

yarn e2e:interactive

Which will bring up the Cypress UI which you can use to select which tests to run and see the Cypress test runner interacting with the browser.

Writing tests

If you are creating a new feature then you should ensure that you add the relevant tests for said code. Most tests written will be unit tests, with e2e tests written instead for ensuring user stories are achieved and to test code that is hard to test in unit tests.

In general, the testing and assertion libraries make it relatively easy to read tests as the assertions are meant to emulate human reasoning e.g. expect(2 + 2).toEqual(4) and so looking at other test files can be useful to figure out how to do something if what you're testing has similarities to other parts of the app.

Unit tests

When you create a new component myComponent.tsx, a corresponding myComponent.test.tsx file should be created in the same folder. Whilst developing tests, then running the tests in watch mode is useful as every time you save either your test file or the files you are testing the tests will rerun. To do so, run the following command:

yarn test:watch

You can select options using w so do things like only run tests on files that have changed, or only run failed tests, or run specific files or tests based on regex matching filenames or testnames. Additionally, you can choose to run a single test file not in watch mode by passing a subset of the name of the test file to the test command e.g.

yarn test myComponentTests

You should then write tests using the Jest test framework and Enzyme, if you are new to Jest & Enzyme then the Flavio Copes Jest tutorial and Scotch Enzyme and Jest tutorial are a good start, but in general the process goes as follows:

  1. Use describe to group relevant tests together e.g.

    describe('My Component', () => {
       // your tests here
    });
    
  2. Use beforeEach to set up data before each test runs

    • e.g. Set up shallow for shallow rendering and mount for full DOM rendering
      let shallow;
      let mount;
      beforeEach(() => {
        shallow = createShallow({});
        mount = createMount();
      });
      
    • e.g. Set up mock redux store
      beforeEach(() => {
        mockStore = configureStore();
        state = JSON.parse(JSON.stringify({ daaas: initialState }));
      });
      
  3. write a singular test case with a descriptive name

    it('should render correctly', () => {
       // your test code here
    });
    
  4. Shallow render or mount your component depending on what you test does. Mount actually renders the DOM code - so DOM interactions like clicks, or rendering subcomponents need mount, otherwise use shallow e.g. if your component needs the store:

    it('should render correctly', () => {
       const wrapper = shallow(<MyComponent store={mockStore(state)} />);
    });
    

    or

    it('should render correctly', () => {
       const wrapper = mount(
         <Provider store={mockStore(state)}>
           <MyComponent />
         </Provider>
       );
    });
    
  5. Assert something about your component or interact with it and assert

    e.g. Snapshot test - tests that your component looks the same - this should be done for most components

    it('should render correctly', () => {
       const wrapper = shallow(<MyComponent store={mockStore(state)} />);
       expect(wrapper.dive().dive()).toMatchSnapshot();
    });
    

    e.g. clicking a button sends an action

    it('sends action when button clicked', () => {
      const testStore = mockStore(state);
      const wrapper = mount(
        <Provider store={testStore}>
          <MainAppBarComponent />
        </Provider>
      );
    
      wrapper
        .find('button')
        .first()
        .simulate('click');
    
      expect(testStore.getActions().length).toEqual(1);
      expect(testStore.getActions()[0]).toEqual(action());
    });
    
    

Code coverage

A summary of the code coverage is available after running unit tests. For details information regarding the uncovered lines look into the code coverage report that is generated at the location: [package name]/coverage/lcov-report/index.html

What to do when snapshot tests fail

When snapshots fail, that means that the underlying DOM structure of your component has changed. You need to confirm that this change was intended, e.g. you added a new component or property or something, or if it is something you didn't intend then you need to figure out why it changed and fix the issue (if it is an issue - sometimes the underlying structure changed due to changes in Material UI or another library and you can just update snapshots).

To update all failing snapshots, run:

yarn test -u

and commit the changes.

To interactively change snapshots i.e go through one by one and accept and reject changes, then enter test watch mode:

yarn test:watch

Then bring up the watch menu by pressing w to see all your options and then press i to select the interactive snapshot update mode.

End-to-end (e2e) tests

When a new user story thought to have been completed, then before it can be marked as complete an e2e test should be created. Also, if there is a part of a component that cannot be tested by an unit test, then writing an e2e test for that test case is also advised.

Whilst writing e2e tests, it is recommended to use the interactive mode of Cypress as this means you won't have to rebuild your server every time and can use the Cypress test tools and browser developer tools to help you (e.g. find css selectors). As a reminder, the interactive test command is shown below:

yarn e2e:interactive

There are a couple of things that you can do if you ever get a Cypress failed to start error message while running the above command:

  • Make sure that the cypress versions are correct in the package.json and yarn.lock files. If they are then telete the node_modules directory and run yarn install.
  • If the above does not fix the issue then clear the cypress cache by running yarn cypress cache clear. Note that you may also need to run yarn cypress install.

You should then write tests using the Cypress test framework, if you are new to Cypress then the Cypress docs are a good start, but in general the process goes as follows:

  1. Create a new test spec in cypress/integration with the name format test_name.spec.ts
  2. Use describe to group relevant tests together e.g.
    describe('Login', () => {
       // your tests here
    });
    
  3. If needed, use beforeEach to set up data before each test runs. See the unit test docs above for an example
  4. write a singular test case with a descriptive name
    it('should login given correct credentials', () => {
       // your test code here
    });
    
  5. Visit a page, and interact with it and assert things based on said interactions
    it('should login given correct credentials', () => {
      cy.visit('/login');
      cy.get('button[aria-label="Open navigation menu"]').should(
        'not.be.visible'
      );
    
      cy.contains('Username*')
        .parent()
        .find('input')
        .type('username');
      cy.contains('Password*')
        .parent()
        .find('input')
        .type('password');
    
      cy.contains('Username*')
        .parent()
        .parent()
        .contains('button', 'Sign in')
        .click();
    
      cy.url().should('eq', 'http://127.0.0.1:3000/');
    
      cy.window().then(
        window => expect(window.localStorage.getItem('daaas:token')).not.be.null
      );
      cy.get('button[aria-label="Open navigation menu"]').should('be.visible');
    });
    
Clone this wiki locally