Testing with Bruno


Uncategorized

Updated Dec 22nd, 2021

Bruno Antunes has a series of videos on React Testing. The “setup” environment for Typescript and React is a huge time saver. Also may need to refer to RTL docs or Jest docs.

Introduction to Testing: Concepts for Beginners – React.js Testing Tutorial #1

E2E testing: Automated

Integration vs Unit: Kent C. Dodd’s take, “Don’t care about distinctions.”

Example with car on assembly line, test engine before putting in the car. Who cares what type of test.

What is mocking? Battery in the car. Test shouldn’t rely on the battery because it might be dead so it gets connected to a power outlet.

When and why to test? High probability and high risk. Amazon profile picture doesn’t need to be tested but their shopping cart absolutely does.

The pyramid of testing: Kind of BS, more like a diamond. Creator of NextJS: “Write tests, not too many. Mostly integration.”

Example of Error in Testing. He was testing the implementation of Axios and they moved to Fetch and it broke all their tests. Do not test implementation details.

Test the public interfaces, the components and the props they receive.

Setup Testing Env: Jest, React Testing Library, eslint, GitHub Actions -React.js Testing Tutorial #2

Why not use default create-react-app or default next-js setup? Need tests with typescript need to fail compile time?

npx create-next-app yourProjectName --ts

// cd into project
 
npm install --save-dev jest typescript ts-jest @types/jest

npx ts-jest config:init // creates "jest.config.js" file

// create "test" script in package.json

npm install --save-dev @testing-library/user-event
npm install --save-dev @testing-library/dom
npm install --save-dev @testing-library/jest-dom

Bruno always creates src folder and moves pages into that folder

Create a simple test to make sure jest and ts-jest is working

create test script

npm t is the same as npm run test

install react-testing-library

npm install --save-dev @testing-library/react

// but Bruno also likes to install userEvents and testing-library/dom and jest-dom

@testing-library/user-event
@testing-library/dom
@testing-library/jest-dom

Create another test with react component

Test fails in error beacuse with Next, in tsconfig.json the “jsx”:”preserve” line (cost Bruno a few hours). To solve this create a tsconfig.jest.json file and add:

{
  "extends":"./tsconfig.json",
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

*Note: this could be called tsconfig.test.json but Bruno likes to have a separate one for cypress and jest.

The test will still fail in error because you need to tell ts-jest and jest that you have this new config file.

go to test-jest docs and see options has tsconfig and shows you how can override. copy and paste into jest.config.ts file:

globals: {
  'ts-jest': {
  tsconfig: './tsconfig.jest.json',  
  }
}

still fails because testEnvironment: “node”, and needs to be changed to “jsdom” which we need to install.

Search google for “setupFilesAfterEnv,” to have jest run once before runnign and tests. copy and past line from docs:

setupFilesAfterEnv: ['./src/jest.setup.ss']

Bow in “jest.setup.ts” file, import ‘@testing-library/jest-dom’

Good but now we need ESLINT config which is just as important.

npx eslint–init and eslint will start to ask a few questions.

in “.eslintrc.js” file, in the “extends” property add “next”, and “next/core-web-vitals”

Then remove the “.env” object and the plugins object. Add back a plugin in the “extends” area:
“plugin:@typescript-eslint/recommended-requiring-type-checking”

make sure there is a “lint”:”next lint” script in the package.json file and run.

Then back in “eslintrc.js” go back to the “parserOptions” field and add “project”: “./tsconfig.json”

disable an ESlint rule (may do this a few times)

"rules": {
  "@typescript-eslint/explicit-module-boundary-types": "off"
}

Configure next with ESLINT becaise they are only looking at a few folders by default (pages/, components/, and lib/). Add –dir src to the “lint” script in the package.json fiel since Bruno pout pages in src folder.

install eslint-plugin-jest plugin to use both recommended and style config. This is done via the “.eslintrc.js” file AND running the npm command npm install eslint-plugin-jest –save-dev

install react-testing-library eslint plugin

Create a lint stage

Use github actions to run lint and prettier.

React Testing Library for Beginners: React.js Testing Tutorial #3

Test Driven Development: TDD

Red Green Refactor

The goal for the React testing library is to test React components but focus less on the implementation details and more on the way your software is used. To prevent getting burned on a change. For example, class-based components moving to functional components. RTL is a replacement for Enzyme.

Grab/Query elements from the screen. There is a priority recommended. The best is getByRole, then getByText. Using the “data-testid” and “getByTestId” is the last priority but has its use cases.

Testing-playground.com

Do your tests based on the expected outcome, not based on the implementation.

Add a new file “Counter.spec.tsx” or “Counter.test.tsx”

test() is just syntactic sugar for it()

it("defaultCount=0, and + clicked then counter = 1", () => {
  render(<Counter defaultCount={0} description="My Counter"/>)
  expect(screen.getByText("Current Count: 0"))
  expect(screen.getByText(/My Counter/i)).toBeInTheDocument
})

Note: The test above uses regex in forward brackets and the /i is saying case sensitivity does not matter.

.todo can help you delay your test. they will show in separate color in the terminal.

fireEvent.click

grouping using describe() create sections and this helps to find tests easier.

use beforeEach() to run every time before the rest pf the group.

Add an “aria-label” attribute to the button and grab with getByRole

Add new feature using TDD.

React Async Testing using React Testing Library for Beginners: React.js Testing Tutorial #4

Testing asynch code is where people struggle the most in their React testing journey.

What is async code? Clicking increment button and it calls to the server to see what the next number is, introducing some delay into your code. Other examples include using a setInterval or setTimeout or using a debounce when entering text into a text field. Need to wait for an action to happen before you test your code.

We don’t need to use “sleeps” like you may see in Selenium because RTL has “findBy Queries” which go asynchronously as compared to the “getBy” which go synchronously.

Note: There are “getBy, findBy, queryBy, getAllBy, findAllBy, queryAllBy” query methods.

“findBy” queries work when you expect an element to appear but the change to the DOM may not happen immediately.

The default interval is 50ms. However it will run your callback immediately before starting the intervals. The default timeout is 1000ms. This means that react-testing will test to see if something is in the DOM, and if not, will check again in 50ms.

Example 1:

use findBy query

convert previous example to have the click action in a “setTimeout” and this makes some tests fail because they immediately expect the counter to be one.

Note: How can you focus a test to have it run first? Use “.fit” instead of “.it” but know that the other “beforeEach” will still run.

fit('renders "Current count: 1"', async() => {
  await waitFor(() => expect(screen.getByText('Current Count: 1')).toBeInTheDocument())
})

Note: to only run the tests in one file use the command “npm t — yourFilename.spec.tsx”

Bruno prefers, when here are a lot “beforeEach”(s) depending on each other, to have the “beforeEach” to have all the logic regarding the waiting and doing actions. This way all you tests can just have the synchronous stuff and not in each “.it()” although not everyone will agree with this strategy.

~12.5 minutes

How you can wait for elements to be removed from the screen? For example, you do an http call and you have a spinner on the screen. How do you make sure the spinner was there and now you are waiting for it to disappear only when it disappears you want your test to pass?

Example 2: Add an element to act as a spinner that disappears once the counter gets to 15, 300ms after the http is done disappear.

Add some state to act as loading state.

it("renders too big, and will disappear after 300ms", async () => {
  await waitForElementToBeRemoved(() => screen.queryByText("I am too small"))
})

Note: waitForElementToBeRemoved will check to see if the element was ever there and then remove it. In the case of a loading spinner it may not show until a fecth button is clicked and then when the http request is finished updates the state to remove the loading spinner.

You set a variable named “id” using let. It is later assigned to a “setTimeout.” What type should you use next to id?

useEffect(() => {
  let id = NodeJS.Timeout
  id = setTimeout(() => setLoading(false), 300)
  return function tearDown() {
    clearTimeout(id)
  }
}, 300)

How can run a single test in a single file using jest? instead of runnign all of your tests, or even all of the tests in one file?

use the fit() keyword and npm t –yourFileName.spec.tsx

React may throw warning if we are trying to access/test a component after is has been unmounted/destroyed. Need a cleanup function that clears the timeout.

As an example let’s say you increment a value in state when a button is clicked but that update function is in a setTimeout. Your tests will fail because the value does not update immediately.

When you focus a test with the .fit keyword will the beforeEach’s still run? Yes

findBy returns a promise so can use await keyword

default interval 50ms
default timeout 1000ms

syntactic sugar on top of waitFor

getBy and findBy queries return what when they don’t find a match?

throw an error. queryBy just returns null.

Mocking React Components and Functions using Jest for Beginners – React.js Testing Tutorial #5

Example #1:

Always want Math.random() to return a specific value so you can run your test.

jest.spyOn(Math, "random").mockReturnValue(0.5)
// toHaveBeenCalledTimes(1)
// toHaveBeenCalledWith()
// toHaveBeenLastCalledWith()

“Spies keep for forever counts from the past so a lot of times you need to clear a mock (telling it to forget about the past) to make sure other tests don’t fail. This is often done in a “beforeEach()”.

// mockClear()
// mockReset()
// mockImplementation is an alternative to .mockReturnValue()

beforeEach(() => whatEvs.mockClear())

Example #2:

Two parts, the first is just a button that calls an “onMoney” function. We want to test that when we click the button the function is called. There are two ways to do this. The first way involves asynchronous testing using “done” keyword. Pass “done” as a parameter and then later calling “done()”. The second way is to use from jest the “jest.fn()” syntax that can be called with “.toHaveBeenCalledTimes(1)” or “toHaveBeenCalledWith(33)”

//first way

describe('MyComponent', () => {
  it('renders Material-UI grid with columnDefs and rowData', (done) => {
    const myOnMoney = (n: number) => {
      expect(n).toBe(33)
      done()
    };
    render(<Example2 onMoney={myOnMoney} />);
    fireEvent.click(screen.getByRole('button', { name: 'Give me 33 dollars' }));

  });
})
//second way

describe('MyComponent', () => {
  beforeEach(() => {
    mockedDataGrid.mockClear();
  });

  it('renders Material-UI grid with columnDefs and rowData', () => {
    const myOnMoney = jest.fn();
    render(<Example2 onMoney={myOnMoney} />);
    fireEvent.click(screen.getByRole('button', { name: 'Give me 33 dollars' }));
    expect(myOnMoney).toHaveBeenCalledTimes(1);
    expect(myOnMoney).toHaveBeenCalledWith(33);
  });
})

The second part of the example is to mock a “DataGrid” component from material UI in which we mock this module to just render a div that says table. We also want to make sure we pass the appropriate props. As long as we pass the expected props we fully trust that the material UI team has done their tests, and so we don’t have to more than make sure we are sending the necessary props.

Bruno also mentioned the use of expect.objectContaining() to have required fields for the column of the DataGrid, helpful if you only care abouyt one or two of the properties you don’t need to list them all.

Another important thing to prevent this test from failing is to add an empty pair of curly brackets as a dependency array. This is because when react calls our components it does so in a context.

import { Example2, rows } from './Example2';
import { fireEvent, render, screen } from '@testing-library/react';
import { mocked } from 'ts-jest/utils';
import { DataGrid } from '@material-ui/data-grid';

jest.mock('@material-ui/data-grid', () => ({
  ...jest.requireActual('@material-ui/data-grid'), // get the module as is
  DataGrid: jest.fn(() => <div>Table</div>),
}));

const mockedDataGrid = mocked(DataGrid);
// with the line above, now we can do the same things with the function
// toHaveBeenCalledTimes(1) and toHaveBeenCalledWith() 

describe('MyComponent', () => {
  beforeEach(() => {
    mockedDataGrid.mockClear();
  });

  it('renders Material-UI grid with columnDefs and rowData', () => {
    const myOnMoney = jest.fn();
    render(<Example2 onMoney={myOnMoney} />);
    fireEvent.click(screen.getByRole('button', { name: 'Give me 33 dollars' }));
    expect(myOnMoney).toHaveBeenCalledTimes(1);
    expect(myOnMoney).toHaveBeenCalledWith(33);
  });

  it('renders table passing the expected props', () => {
    render(<Example2 onMoney={jest.fn()} />);
    expect(mockedDataGrid).toHaveBeenCalledTimes(1);
    expect(mockedDataGrid).toHaveBeenLastCalledWith(
      {
        rows: rows,
        columns: [
          expect.objectContaining({ field: 'id' }),
          expect.objectContaining({ field: 'firstName' }),
          expect.objectContaining({ field: 'lastName' }),
          expect.objectContaining({ field: 'age' }),
        ],
        pageSize: 5,
        checkboxSelection: true,
      },
      {}
    );
  });
});

Note: About jest.fn(), we can pass this as a prop to simplify our tests, and just test the links between code by erasing the actual implementation of a function. See below:

it("render the comp passing the expected props", () => {
  render(<WhatEvCompExpectingProps onMoney={jest.fn()} />)
})

Example #3:

Similar to example 2 but about “swipeable drawer” from material-ui. Github. He received request to specifically mock this component.

import { render, screen } from '@testing-library/react';
import React from 'react';
import { MyDrawer } from './Drawer';
import user from '@testing-library/user-event';

jest.mock('@material-ui/core', () => ({
  ...jest.requireActual('@material-ui/core'),
  SwipeableDrawer: jest.fn(() => <div>HELLOOOOOO</div>),
}));

describe('Drawer', () => {
  it('shows no "Hello YouTube!"', () => {
    render(<MyDrawer />);
    expect(screen.queryByText('HELLOOOOOO')).toBeInTheDocument();
  });

  it('clicking on "Open Drawer" Button shows "Hello YouTube!"', () => {
    render(<MyDrawer />);
    user.click(screen.getByRole('button', { name: 'Open Drawer' }));
    expect(screen.getByText('HELLOOOOOO')).toBeInTheDocument();
  });
});

I saw this code and thought it may be useful:

expect(screen.queryByText("Hello Youtube!")).not.toBeInTheDocument()

Also this:

user.keyboard('{escape}')

Example #4:

Similar to example 3 but we are going to mock something from our own application.

import { render, screen } from '@testing-library/react';
import { mocked } from 'ts-jest/utils';
import { MyDrawer } from '../Example3/Drawer';
import { Example4 } from './Example4';

jest.mock('../Example3/Drawer');
mocked(MyDrawer).mockImplementation(() => <div>mocked: drawer</div>);

describe('Example4', () => {
  it('renders MyDrawer', () => {
    render(<Example4 />);
    expect(
      screen.queryByText('Hello Drawer Component!')
    ).not.toBeInTheDocument();
    expect(screen.getByText('mocked: drawer')).toBeInTheDocument();
  });
});

Example #5:

When requesting mock from very complex component (very deeply nested?). Uses “mocks” folder.

The test file:

import { render, screen } from '@testing-library/react';
import { Example5 } from './Example5';

jest.mock('../../VeryComplex/DeepFolder/DeeperFolder/VeryComplex');

describe('Example 5', () => {
  it('renders very complex component', () => {
    render(<Example5 />);
    expect(screen.getByText('SIMPLE VERSION')).toBeInTheDocument();
  });
});

The component file:

import { VeryComplex } from '../../VeryComplex/DeepFolder/DeeperFolder/VeryComplex';

export function Example5() {
  return (
    <div>
      <VeryComplex />
    </div>
  );
}

Mock HTTP calls using Fetch or Axios – Mock Service Worker – React.js Testing Tutorial #6

Avoid using mocks for http calls. Use MSW, “mock service worker” library, (mswjs.io).

Avoid mocking axios or fetch with syntax like this:

jest.mock('axios')
// jest.spyOn(window, 'fetch')

const mockedAxios = mocked(axios);
const mockedAxiosGet = mocked(mockedAxios.get)
const mockedAxiosPost = mocked(mockedAxios.post)

/*

describe("PhotoList", () => {
 beforeEach(() => {
  mockedAxiosGet.mockResolvedValue({
   data: [
     {
       id: 1,
       thumbnailUrl: "/photo1.png",
       title: "Hello World",
       favorite: false,
     },
     [] as Photo[],
   ]
  })
 })
})
*/

The reason to use MSW is making HTTP calls with Axios vs Fetch is an implentation detail so don’t we don’t base our test on that but base on the http call itself. If we ever change from fetch to axios we don’t invalidate our tests.

Install the MSW library and a polyfill:

npm install --save-dev msw whatwg-fetch

Using fetch with MSW will require a polyfill imported into your “jest.setup.ts” file.

import "whatwg-fetch"

Note: in order to have a “jest.setup.js” file you need to add into your “jest.config.js” file the following line:

setupFilesAfterEnv: [".src/jest.setup.ts"]

Here is the link to the examples.

Will use beforeAll() and afterAll() to start and stop server and likely afterEach() as well.

MSW Docs: How to Create our Mock Server:

https://mswjs.io they have a REST API example using TypeScript:

import { setupWorker, rest } from 'msw'
interface LoginBody {
  username: string
}
interface LoginResponse {
  username: string
  firstName: string
}
const worker = setupWorker(
  rest.post<LoginBody, LoginResponse>('/login', (req, res, ctx) => {
    const { username } = req.body
    return res(
      ctx.json({
        username,
        firstName: 'John'
      })
    )
  }),
)
worker.start()

MSW HTTP Get Call – Create Mock Server:

Slightly different for Node:

import {setupServer} from "msw/node"
import {rest} from "msw"

const server = setupServer(rest.get('endpoint', (req, res, ctx) => {
  return res(ctx.json([]))
}))

Example #1: Spinner test and add delay to MSW response

Example #2: Type “Bruno” in search field and use query parameters

May need to wait for multiple calls to end to finish loading spinner. Instead of using loading state with true and false use how many calls are pending using a number. Another way is to cancel previous calls as they are made.

Example #3: Return 500 status code with MSW

Example #4: HTTP Post request with MSW

Uses .resetHandlers() and after seeing test passes, switches to fetch implementation and the test still passes.

React Hooks SWR: Test components that useSWR – Mock Service Worker – React.js Testing Tutorial #7

Using fetch with MSW will require polyfill imported into jest.setup.ts file.

Create setupServer()

SWR does caching so may need to override with {dedupingInterval: 0, provider: () => new Map()} and to not repeat yourself you have two options: AllTheProviders or instead create a customRender (both from React Testing Lib).

Testing React Forms – React Testing Library – React.js Testing Tutorial #8

multistep form

fireEvent.change but instead user from “@testing-library/user-event”

.type()
.click()
clickNextButton()
waitFor()
expect()
toHaveBeenCalledTimes(1)

formik validations are asynchronous, so there is a very slight delay.

refactor to use toHaveErrorMessage, require aria-errormessage and an id


Top