Jest: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
 
(2 intermediate revisions by the same user not shown)
Line 379: Line 379:
To solve this problem we need to transpile (convert) the module from esm to commonJS. To do this we need to config jest like this.
To solve this problem we need to transpile (convert) the module from esm to commonJS. To do this we need to config jest like this.
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
export default async (): Promise<Config> => {
export default async (): Promise<Config> => {
     return {
     return {
        rootDir: '../',
         verbose: true,
         verbose: true,
         setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
         setupFilesAfterEnv: ['<rootDir>/configs/setupTests.ts'],
         testEnvironment: '<rootDir>/jest.environment.js',
         testEnvironment: '<rootDir>/configs/jest.environment.ts',


         moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
         moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
Line 392: Line 395:
         transform: {
         transform: {
             '^.+\\.(ts|tsx)?$': 'ts-jest',
             '^.+\\.(ts|tsx)?$': 'ts-jest',
             '^.+\\.(js|jsx)$': 'babel-jest',
             '^.+\\.(js|jsx)$': ['babel-jest', { configFile: './configs/babel.config.js' }],
             '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
             '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
                 '<rootDir>/__mocks__/fileMock.js',
                 '<rootDir>/configs/__mocks__/fileMock.js',
             '^.+\\.(css|sass|scss)$': '<rootDir>/__mocks__/styleMock.js',
             '^.+\\.(css|sass|scss)$': '<rootDir>/configs/__mocks__/styleMock.js',
         },
         },
        testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],


         transformIgnorePatterns: ['/node_modules/(?!(uint8array-extras|msal-react-tester)/)'],
         transformIgnorePatterns: ['/node_modules/(?!(uint8array-extras|msal-react-tester)/)'],
    }
...
}
}
</syntaxhighlight>
</syntaxhighlight>
The important lines is the transformIgnorePatterns which tells jest to ignore all modules except uint8array-extras or msal-react-tester. I am hopeless at regex so went there to prove it. You can see only the last two are selected and therefore ignored.
The important lines is the transformIgnorePatterns which tells jest to ignore all modules except uint8array-extras or msal-react-tester. I am hopeless at regex so went there to prove it. You can see only the last two are selected and therefore ignored.
[[File:Reg101 image1 jest.png | 400px]]
[[File:Reg101 image1 jest.png | 400px]]
==Other Configs==
There are other configurations involved too and are provided here for completeness
*setupTests.ts
*jest.environment.ts
<syntaxhighlight lang="ts">
// setupTests.ts
import '@testing-library/jest-dom'
import crypto from 'crypto'
Object.defineProperty(window, 'crypto', {
    value: {
        randomUUID: () => crypto.randomUUID(),
    },
})
</syntaxhighlight>
<br>
<syntaxhighlight lang="ts">
// jest.environment.ts
import Environment from 'jest-environment-jsdom'
export default class CustomTestEnvironment extends Environment {
    async setup() {
        await super.setup()
        this.global.TextEncoder = TextEncoder
        this.global.TextDecoder = TextDecoder
        this.global.Response = Response
        this.global.Request = Request
    }
}
</syntaxhighlight>

Latest revision as of 05:22, 4 December 2024

Introduction

This is a page to introduce Jest. Facebook employs a lot of the people who develop jest and can be and is used for test in other frameworks.

Example code can be found at [here]

Choices

Popular tools for testing are Enzyme and Jasmine/Mocha. Jest is built on top of Jasmine/Mocha. Jest works with or without React and has a great assertion library.

Why Not Enzyme

This has over 200+ issues open and 26 of these are bugs. Jest is recommended by the react team.

Jest vs Mocha

Jest and Mocha both run tests sync and async but jest also has the following features.

  • Spies (stubbing methods to spy)
  • Snapshot testing
  • Module mocking (stubbing objects)

Getting Started

Running

We can run jest on it's own or continually with watch mode

jest --watch

At least one "it" block must be found or jest will return an error.

Code Coverage

With Jest we can get a report by typing

npm test  -- --coverage

Naming Test

The file names must be recognized as a test.

 __tests__/*.js
 *.spec.js
 *.test.js

Example Test in Jest

Describe is the suite, it is the test

describe("The question list ", ()=> {
    it ("should display a list of items", ()=> {
        expect(2+2).toEqual(4);
    })

    it ("should display a list of items", ()=> {
        expect(2+4).toEqual(6);
    })
})

Setup and Teardown

This can be done using before each and before all.

BeforeEach BeforeAll

beforeAll(()=>{
   console.log("Hello");
});

and to tear down we can use AfterEach AfterAll

afterAll(()=>{
   console.log("Hello");
});

Note does not matter what order you write them in.

Skipping and Isolating Tests

Add keyword only to include tests

it.only ("should display a list of items", ()=> {
    expect(2+2).toEqual(4);
})

Add keyword skip to exlude tests

it.skip ("should display a list of items", ()=> {
    expect(2+2).toEqual(4);
})

Async Tests

// Using a callback that is passed to the test force it to wait
it('async test 1', done=> {
    setTimeout(done, 100)
});

// Return a Promise which jest knows to wait for
it('async test 2',()=> {
    return new Promise(
        resolve => setTimeout(resolve,100)
    );
});

// Use await to wait before returning.
it('async test 1', async ()=>
    await delay(100)
);

Mocking

Why

  • Reduces dependencies and makes testing faster
  • Prevents side effects
  • Control the scenario we wish to test e.g. timeout, corrupt data

What is Mocking

A mock is a replacement for known functionality which can be used to capture usage of the original function and fake a response to the users of the mock.

Setup

Create directory __mocking__ at the same level as npm node_module you are trying to mock. This makes your module get loaded in place of the original module.

Example

Function to Mock

/**

* Fetch question details from the local proxy API
*/
export function * handleFetchQuestion ({question_id}) {
    const raw = yield fetch(`/api/questions/${question_id}`);
    const json = yield raw.json();
    const question = json.items[0];
    /**
     * Notify application that question has been fetched
     */
    yield put({type:`FETCHED_QUESTION`,question});
}

Mocking the JavaScript Fetch function

In the code below we are attempting to mock the javascript fetch function where call the function and return a value. I orginally struggled with this because of the old syntax but it became a lot clear after so much work with node js and modern javascript

let __value = 42;

// Old syntax

// Replace a function which returns 42
const isomorphicFetch = jest.fn( (){ returns __value });

// Add a function to set the value
isomorphicFetch.__setValue = function(v) {__value = v};

// Modern notation
const isomorphicFetch = jest.fn( ()=> __value);
isomorphicFetch.__setValue = v => __value = v;

export default  isomorphicFetch;

Mocking the handleFetchQuestion

Now we need to mock the original function above using our ownversion of fetch. We set the value to be the format expected back from fetch which is an object which we can do because __setValue is not type aware [{"question_id": 42}]

import { handleFetchQuestion }  from './fetch-question-saga';
import fetch from 'isomorphic-fetch';
/**
 * This test is an example of two important Jest testing principles,
 * 1) we're mocking the "fetch" module, so that we don't actually make a request every time we run the test
 *  The module, isomorphic fetch, is conveniently mocked automatically be including the file __mocks__/isomorphic-fetch.js adjacent to to the Node.js folder
 * 2) we're using an async function to automatically deal with the fact that our app isn't synchronous
 */
describe("Fetch questions saga",()=>{
    beforeAll(()=>{
        fetch.__setValue([{question_id:42}]);
    });
    it("should get the questions from the correct endpoint in response to the appropriate action",async ()=>{
        const gen = handleFetchQuestion({question_id:42});
        /**
         * At this point, isomorphic fetch must have been mocked,
         * or an error will occur, or, worse, an unexpected side effect!
         */
        const { value } = await gen.next();
        expect(value).toEqual([{question_id:42}]);

        /**
         * We can also assert that fetch has been called with the values expected (note that we used a spy in the file where we mock fetch.)
         */
        expect(fetch).toHaveBeenCalledWith(`/api/questions/42`);
    });
});

Snapshot Testing

It is really hard to be excited about this. It seems so fragile. Basically it records the first run and makes comparisons on subsequent runs. Any change is flagged.

import React from 'react';
import TagsList from './TagsList'
import renderer from 'react-test-renderer';


describe("The tags list",()=>{
    /**
     * The tagsList can be tested against an expected snapshot value, as in below.
     */
   it ("renders as expected",()=>{
       const tree = renderer
           .create(<TagsList tags={[`css`,`html`,`typescript`,`coffeescript`]}/>)
           .toJSON();

       expect(tree).toMatchSnapshot();
   });
});

Component Testing

See also React_Testing_Components

Making Components More Testable

From best to worse.

  • No internal state - output is a idempotent product of the props
  • No side-effects, have these handled by sagas, thunks etc
  • No lifecycle hooks - fetching data handled on application level

Component Testing with Redux

React and Redux

These a perceived as a great match because it means

  • Components do not generate side effects, only actions
  • Components consist of display and container components
  • No state in Components

So the approach to testing for React Redux Components is

  • Test Container and Display Separately
  • Use Unit test to verify methods and props on container
  • Use snapshot tests for the display

Redux and Testing mapStateToProps

mapStateToProps is the function responsible for mapping state to the props. Our focus on testing here is only the mapping and not the content. i.e. that the state is appropriately mapped to the props.

import { mapStateToProps } from '../QuestionDetail';

it("should map the state to props correctly", () => {

   // Sample App State with sampleQuestion being object in state
   const sampleQuestion {
       question_id:42,
       body: "Space is big"
   };

   const appState = {
       questions: [sampleQuestion]
   };
   
   // Sample Props
   const ownProps = {
       question:42
   });

   const componentState = mapStateToProps(appState, ownProps)
   expect(componentState).toEqual(sampleQuestion);
}

Component Testing with State

Bit of a lot of code but here goes

Create component

import React from 'react';

export default class extends React.Component {
    constructor)(...args) {
        super(...args);
        this.state = {
            count: -1
        }
    }

    async comonentDidMount () {
        let { count } = await NotificationService.GetNotification();
        this.setState( {
            count
        });
    }

    render() {
       return (
           <section className="mt-3 mb-2">
               <div className="notifications">
                   {this.state.count != -1 
                   ? '${this.state.count} Notifications Awaiting!' 
                   : 'Loading...'}
               </div>
           </section>
       )
    }
}

Create a Service

export default {
    async GetNoitifications() {
        console.warning("REAL NOTIFICATION SERVICE!");
        await delay(1000);
        return { count: 42 };
    }
}

Test Case

And here is the test case

import NotificationsViewer from '../NotificationsViewer';
import renderer from 'react-test-renderer';
import React from 'react';
import delay from 'redux-saga';

// NOTE Local module you need to pass in url of notification service
jest.mock('../../services/NotificationService');

// NOTE 2 this needs to be a require and needs to be after the jest. Making it
// an import will fail

const notificationService = require('../../services/NotificationService').default; 

  
describe("The notification viewer", () => {

   beforeAll( ()=> {
     notificationService.__setCount(5);
   });

   it("should display the correct number of notifications", async()=> {

       const tree = renderer
           .create(
               <NoticationsViewer/>
            )
       await delay();
       const instance = tree.root;

       // find by className the component
       const component = instance.findByProps( { className: 'notificiations' });  

       const text = component.children[0];
       export(text).toEqual("5 Notifications");

   });
});

Mocked Service

Let's make a mocked service in the appropriate place next to the real service. src\services\__mocks__\NotificationService.js

let count = 0;
export default {

    __setCount(__count) {
        count = _count;
    },
    async GetNotifications() {
        console.warn("Good Job! Using Mock");
    }
}

Matchers in Jest

These can be found [1]

Some of them for quick reference.

expect(value)
expect.extend(matchers)
expect.anything()
expect.any(constructor)
expect.arrayContaining(array)
expect.assertions(number)
expect.hasAssertions()
expect.not.arrayContaining(array)
expect.not.objectContaining(object)
expect.not.stringContaining(string)
expect.not.stringMatching(string | regexp)
expect.objectContaining(object)
expect.stringContaining(string)
expect.stringMatching(string | regexp)
expect.addSnapshotSerializer(serializer)

Transpiling in Jest 2024

I am certainly not expert but here is what I know. There several types of formats for modules,

  • commonJS (uses require)
  • esm (uses import)

Introduction to issue

When you run jest you may sometimes see

This is because the module, in this case MsalReactTester, cannot be loaded because it is an esm module and you are running jest with commonJS.

Solution

To solve this problem we need to transpile (convert) the module from esm to commonJS. To do this we need to config jest like this.

export default async (): Promise<Config> => {
    return {
        rootDir: '../',

        verbose: true,
        setupFilesAfterEnv: ['<rootDir>/configs/setupTests.ts'],
        testEnvironment: '<rootDir>/configs/jest.environment.ts',

        moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
        moduleDirectories: ['node_modules', 'src'],
        moduleNameMapper: {
            '\\.(css|less|scss)$': 'identity-obj-proxy',
        },
        transform: {
            '^.+\\.(ts|tsx)?$': 'ts-jest',
            '^.+\\.(js|jsx)$': ['babel-jest', { configFile: './configs/babel.config.js' }],
            '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
                '<rootDir>/configs/__mocks__/fileMock.js',
            '^.+\\.(css|sass|scss)$': '<rootDir>/configs/__mocks__/styleMock.js',
        },

        testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],

        transformIgnorePatterns: ['/node_modules/(?!(uint8array-extras|msal-react-tester)/)'],
...
}

The important lines is the transformIgnorePatterns which tells jest to ignore all modules except uint8array-extras or msal-react-tester. I am hopeless at regex so went there to prove it. You can see only the last two are selected and therefore ignored.

Other Configs

There are other configurations involved too and are provided here for completeness

  • setupTests.ts
  • jest.environment.ts
// setupTests.ts
import '@testing-library/jest-dom'

import crypto from 'crypto'

Object.defineProperty(window, 'crypto', {
    value: {
        randomUUID: () => crypto.randomUUID(),
    },
})


// jest.environment.ts
import Environment from 'jest-environment-jsdom'

export default class CustomTestEnvironment extends Environment {
    async setup() {
        await super.setup()
        this.global.TextEncoder = TextEncoder
        this.global.TextDecoder = TextDecoder
        this.global.Response = Response
        this.global.Request = Request
    }
}