0.1.3 • Published 2 years ago

@develemit/ultima v0.1.3

Weekly downloads
-
License
ISC
Repository
github
Last release
2 years ago

Ultima

Why ultima?

An excellent question! Ultima is fundamentally a jest test organizer. It's a different approach to writing tests with the end goal being — write less code to achieve 100% test coverage for React Components.

Getting Started:

First things first, lets get it installed!

npm install --save-dev @develemit/ultima

Next up! We'll need to import @develemit/ultima into your .spec.js

import Ultima from @develemit/ultima';

Ultima is a constructor function which takes a config object to personalize the returned ultima function for your test file.

Ultima takes an object with the following keys:

keyvaluetyperequired
Componentcomponent under testReact Componentyes
defaultPropsprops to be passed to each testObjecthighly suggested!
mockContextValues*default values for your React Context - (React.useContext API)Objectno
setContext*mocked function used to invoke your Context changes - (React.useContext API)functionno
useState*directly passed from React after being locally mocked (examples below)functionno
configdefault config values for your testsObjectno

*For additional examples of mocking context, please see examples/App/App.spec.jsx within this repo.

Ultimately, an ultima function will be able to be destructured from calling new Ultima({//...aboveTableValues}) that you will be able to provide an array of test objects for your testing purposes.

const { ultima } = new Ultima({});

There are two other helper functions that can be destructured from above:

const { ultima, mockUseState, mockSetContext } = new Ultima({});

mockUseState takes any number of arguments which will be the values of the components useState calls. So, for example:

// * Say you have three useStats
const [name, setName] = useState('bob');
const [age, setAge] = useState(99);
const [loggedIn, setLoggedIn] = useState(false);

// Referencing the table below using the "mock" property

{
  title: 'first useState Mock Test!',
  mock: () => mockUseState(
    ['bingo', jest.fn()],
    [23, jest.fn()],
    [true, jest.fn()],
    ),
  //...restOfTestProperties
}
// * Similarly, mockSetContext takes an object which will add/override properties in your mockContextValues

const mockContextValues = {
  name: 'bob',
  age: 99,
  loggedIn: false,
}

{
  title: 'first mockSetContext Mock Test!',
  mock: () => mockSetContext({
    name: 'bingo',
    age: 23,
    loggedIn: true,
  })
  //...restOfTestProperties
}

mockUseState should only be used with useState hook based state (not class based components) For additional examples see src/examples/App.spec.jsx

mockSetContext should only be used with useContext hook based state (not class based components) For additional examples see src/Store.jsx, src/hooks/useStore.js and src/examples/App.spec.jsx

The objects that make up the test array will have the following properties:

keydescriptiontyperequiredExample Value
titlevalue to be used for test namestringyes'age input test'
findfreeform value for finding elements inside the parent componentstring/Component / string/Componentno'.age-input-class-name' / Input / ['.container', '#age-input']
propsprops to be provided to the component (these merge with and override any defaultProps provided)objectno{ loggedIn: false, setAge: mockSetAge }
mockused for any custom mocking/side effects that may need to happen for the current testfunctionno() => { mockSetAge.mockImplementationOnce(() => NaN) }
debugprovides insights into the current test (context/current mock results) - false by defaultbooleannotrue
changesarray of objects used to trigger events or props of componentsarray of objects**no[ { title: 'changes to age 20', event: 'onChange', value: { target: { value: 20'} } }, { title: 'to blah', event: 'onChange', value: { target: { value: 'blah' } } }, ],
expectParamto be passed to jest's expect function ex: expect(expectParam)any | array of anydepends*[mockSetAge, mockSetAge, mockSetAge ]
expectFuncto be passed as the function after jest's expect ex: expect(expectParam)[expectFunc] (default = "toHaveBeenCalledWith")string | array of stringsdepends*['toHaveBeenCalledWith', 'toHaveBeenCalledWith', 'toHaveBeenCalled'] (side note - should you need to mix jest methods that both take and do not take an expected argument, put all methods that don't expect an argument at the end, and only provide values as necessary based on changes length )
expectedto be passed to the expectFunc ex: expect(expectParam)[expectFunc](expected)any | array of anydepends*[20, 40] (side note - You don't need to match the length of expectFunc if the )
rendercan be used to opt out of the ultima test flow and create test cases yourself with the provided ComponentfunctionnoSee render Example Below

* similarly, should the render method be used, expectParam, expectFunc and expected would all not be needed, as native jest can be used inside of the render

** Please reference the table for the shape of the objects for changes Changes can be used in one of two ways.

A) - A more native approach via enzyme, simulating events

B) - Manually triggering props to invoke functions or effects

keydescriptiontypeMethod (A/B)requiredExample Value
titletitle for the individual change teststringbothyes"returns 20"
findto be provided to a querySelector function if needing to find a nested element to trigger eventany | anybothno"#age-input"
propsprops to be used for the current change event (these merge with and override any defaultProps or test level props provided)objectboth{ loggedIn: false }
eventname of the prop to be invokedstringBA - no B - yes'onChange'
valuethe return value from the expectParam (see above)anyBA - no B - yes{ target: { value: 20 } }
atindex of element to trigger simulateintAA - yes B - no0
simulatearguments to be passed to the enzyme simulate functionarray - event: string, value: anyAA - yes B - no['change', { target: { value: 20 } }]
values***array of values to be used to trigger the same event multiple times with different valuesarray - anyBno { target: { value: 20 } }, { target: { value: 40 } }, { target: { value: 60 } }
expectFunc***to be passed as the function after jest's expect ex: expect(expectParam)[expectFunc] (default = "toHaveBeenCalledWith")array - stringBno ['toHaveBeenCalledWith', 'toHaveBeenCalledWith', 'toHaveBeenCalledWith'] (As a side note, this wouldn't really be required as 'toHaveBeenCalledWith' is the default value, however, if the test in question requires a different method for even a single index, it will it will be required to list each needed test method)
expected***to be passed to the expectFunc ex: expect(expectParam)[expectFunc](expected) - mapped 1 to 1 with the index/indices from the array of values and expectParamarray - anyBno[20, 40, 60]

*** if values is to be used, then expectParam, expectFunc and expected are also required

Examples

There are many examples for reference inside of the examples/ folder to see different scenarios but also please feel free to use the below as a reference

const setMessage = jest.fn(); const setShowRemoveModal = jest.fn(); const updateBand = jest.fn();

const defaultProps = { bandSelected: { acntNumb: 'aa11', tokenId: '11aa' }, showRemoveModal: false, setMessage, setShowRemoveModal, updateBand, updateBandCall: jest.fn(), }, };

const { ultima } = new Ultima({ Component: RemoveBandModal, defaultProps, });

const tests = [ { title: 'Buttons', find: Button, props: { updateBand }, changes: [ { title: 'remove_modal_submit false', props: { updateBandCall: jest.fn(() => 'banding!'), }, at: 0, simulate: 'click', }, { title: 'remove_modal_submit error', props: { updateBand: null }, at: 0, simulate: 'click', }, { title: 'remove_modal_submit true', at: 0, simulate: 'click', }, { title: 'remove_modal_cancel', at: 1, simulate: 'click', }, ], expectParam: setMessage, setMessage, setMessage, setShowRemoveModal, expected: 'REMOVE_FAILURE', 'REMOVE_FAILURE', 'REMOVE', false, }, ];

ultima(tests);

</details>


<details>
  <summary>Example using <code>values</code></summary>

```js
import Ultima from '@develemit/ultima';
import Authentication from '.';

const successCallBack = jest.fn();
const failureCallBack = jest.fn();

const defaultProps = {
  id: '',
  successCallBack,
  failureCallBack,
  locale: '',
};

const { ultima } = new Ultima({
  Component: Authentication,
  defaultProps,
});

const tests = [
  {
    title: 'successCallBack',
    changes: [
      {
        title: 'status',
        event: 'onSuccess',
        values: [{ status: 1 }, { status: 1 }, { status: 2 }],
        expectParam: [successCallBack, successCallBack, failureCallBack],
        // expectFunc: 'toHaveBeenCalledWith', -> not needed, as we wanted 'toHaveBeenCalledWith' to be used with each test.
        expected: [1, 1, 2]
      },
    ],
  },
];

ultima(tests);
import React, { useState } from 'react';
import Ultima from '@develemit/ultima';
import BandWearables from '.';
import BandItem from './BandItem';
import { resetAction } from './ActionApiCalls';

let mockUseEffectDependencyArray = '';

jest.mock('react', () => ({
  ...jest.requireActual('react'),
  useState: jest.fn((initial) => [initial, jest.fn()]),
  useEffect: jest.fn((fn, dep) => {
    if (mockUseEffectDependencyArray !== JSON.stringify(dep)) {
      mockUseEffectDependencyArray = JSON.stringify(dep);
      return fn();
    }
    return null;
  }),
}));

jest.mock('./ActionApiCalls', () => ({
  ...jest.requireActual('./ActionApiCalls'),
  resetAction: jest.fn(),
}));

const actions = ['RESET', 'ACTIVATE', 'SUSPEND', 'RESUME', 'REMOVE'];
const bands = [
  {
    tokenId: 'string',
    validThru: '202403',
    actions,
    status: {
      code: 'ACTIVE',
      label: 'ACTIVE',
    },
  },
  {
    tokenId: 'string',
    validThru: '202403',
    actions,
    status: {
      code: 'ACTIVE',
      label: 'ACTIVE',
    },
  },
];

const defaultProps = {
  message: 'New Message',
  bands,
  bandSelected: false,
  locale: 'en-US',
  setBands: jest.fn(),
  setBandSelected: jest.fn(),
  setMessage: jest.fn(),
  setView: jest.fn(),
  updateApiCallOne: jest.fn(() => Promise.resolve([{}])),
  updateApiCallTwo: jest.fn(() => Promise.resolve({ status: 200, body: {} })),
};

const { ultima } = new Ultima({
  Component: BandWearables,
  defaultProps,
  useState, // Ultima needs context for your "useState" function, don't forget to pass it here!
});

const tests = [
  {
    title: 'All actions trigger',
    find: BandItem,
    render: ({ comp }) => { // render returns an object in this shape { comp, main, raw}.
      // The "comp" is either the component in question baseed on the "find" prop (as above, "comp" in this casee is the "BandItem" component), or the primary component for the test cases, "BandWearables" in this case, rendered as a shallow copy.

      // The "main" is always the component provided to the Ultima function, ("BandWearables" in this case) rendered as a shallow copy.

      // Lastly, "raw" is the base provided component ("BandWearables") which is not shallow rendered.w Ideally you shouldn't need "raw" but it may prove useful for certain edge cases in your testing scenarios
      const { actionsFunctions } = comp.at(0).props();
      jest.spyOn(actionsFunctions, 'RESET');
      jest.spyOn(actionsFunctions, 'ACTIVATE');
      jest.spyOn(actionsFunctions, 'SUSPEND');
      jest.spyOn(actionsFunctions, 'RESUME');
      jest.spyOn(actionsFunctions, 'REMOVE');
      actions.map((action, i) => {
        actionsFunctions[action](bands[i % 2 === 0 ? 0 : 1]);
        return expect(actionsFunctions[action]).toHaveBeenCalled();
      });
    },
  },
  {
    title: 'Reset - Error',
    find: BandItem,
    props: { updateApiCallOne: undefined },
    render: ({ comp }) => {
      const { actionsFunctions } = comp.at(0).props();
      jest.spyOn(actionsFunctions, 'RESET');
      actionsFunctions.RESET();
      return expect(actionsFunctions.RESET).toHaveBeenCalled();
    },
  },
  {
    title: 'Reset - Failure',
    find: BandItem,
    props: {
      updateApiCallOne: jest.fn(() => ({
        promise: Promise.resolve([{}]),
      })),
    },
    render: ({ comp }) => {
      const { actionsFunctions } = comp.at(0).props();
      jest.spyOn(actionsFunctions, 'RESET');
      actionsFunctions.RESET();
      return expect(actionsFunctions.RESET).toHaveBeenCalled();
    },
  },
  {
    title: 'Suspend - Error',
    find: BandItem,
    props: { updateApiCallTwo: undefined },
    render: ({ comp }) => {
      const { actionsFunctions } = comp.at(0).props();
      jest.spyOn(actionsFunctions, 'SUSPEND');
      actionsFunctions.SUSPEND();
      return expect(actionsFunctions.SUSPEND).toHaveBeenCalled();
    },
  },
  {
    title: 'Suspend - Failure',
    find: BandItem,
    props: {
      updateApiCallTwo: jest.fn(),
      message: 'SUSPEND_FAILURE',
    },
    render: ({ comp }) => {
      const { actionsFunctions } = comp.at(0).props();
      jest.spyOn(actionsFunctions, 'SUSPEND');
      actionsFunctions.SUSPEND();
      return expect(actionsFunctions.SUSPEND).toHaveBeenCalled();
    },
  },
  {
    title: 'Mobile Coverage',
    props: {
      bands: [
        {
          tokenId: 'string',
          validThru: '202403',
          actions,
          status: 'INACTIVE',
        },
        {
          tokenId: 'string',
          validThru: '202403',
          actions,
          status: 'ACTIVE',
        },
      ],
    },
    expectFunc: 'toBeTruthy',
  },
];

ultima(tests);
import Ultima from '@develemit/ultima';
import { useStore } from 'hooks/useStore'; // Be Sure to import your hook to get your context values
import App from '.';

const setName = jest.fn();
const setAge = jest.fn();

const mockContextValues = {
  name: 'bobo',
  setName,
  setAge,
};

// You have to mock the context hook with your mockContext object prior to using it as setContext in new Ultima()
jest.mock('hooks/useStore', () => ({
  useStore: jest.fn().mockImplementation(() => mockContextValues),
}));

const { ultima } = new Ultima({
  Component: App,
  mockContextValues,
  setContext: useStore, // again... make sure this value is the mocked as above
  config: {
    expectParam: setName,
  },
});

const tests = [
  {
    title: 'name input',
    id: 'name', // the # is prepended to the value
    changes: [
      { title: 'changed to bob', event: 'onChange', value: { target: { value: 'bob' } } },
      { title: 'changed to blah', event: 'onChange', value: { target: { value: 'blah' } } },
    ],
    // expectParam: setName, // Not needed as the config object in the constructor has already applied setName as the default value for expectParam for all tests
    expected: ['bob', 'blah'],
  },
  {
    title: 'age input',
    find: '#age', // different option for finding if you need to be more specific than id (in this case you need to provide the # with the value as with querySelector)
    changes: [
      {
        title: 'to 40',
        value: { target: { value: 40 } },
      },
    ],
    expectParam: setAge,
    expected: 40,
  },
  {
    title: 'exports a function by default',
    expectParam: App,
    expectFunc: 'toBeInstanceOf',
    expected: Function,
  },
];
ultima(tests);

Beyond React Component Testing

For future releases, I would like to expand Ultima React further with an option for redux testing.