Jaia Command and Control (JCC) and Jaia Data Vision (JDV) are React Applications written in Typescript and Javascript. To test these applications we are using Jest as the foundation testing framework with React Testing Library providing ways to interact with React Components. These tools can be used to create unit tests, integration tests, and functional tests.
This document is meant to serve as a starting point for people new to Jest and React Testing Library (RTL). We are still learning the ins and outs of using these tools so our approach and techniques will evolve as we learn. Please update this page with additional information if you learn a new way to do things or exercise more aspects of our applications.
React Testing Library Documentation
Using RTL we want to ensure that our user interfaces work as intended under all circumstances.
Unit testing focuses on a specific software entitiy in isolation and exercises it's functionality by varying inputs and verifying expected outputs. In general, the entitiy will be one of our container modules (src/web/containers
), and the focus of the test may be specific to an individual element in that container. The inputs of any given test are comprised of the Props passed into the container and simulated user actions. The outputs of the test are comprised of updates to props via the use of callback functions and changes to the visual components of the container being tested. An important concept in unit testing is exercising "Edge Conditions". Edge Conditions are sets of inputs and actions that present unique situations likely to cause problems for software. Think "how many different ways does this container need to render?"
Integration testing is similar to unit testing but focuses on the interaction of multiple softeware entities. The test will exercise functiionality of one container and verfiy it's impact on an element of another container. Think "what happens in that container if I do this in this container?"
Functional testing focuses on exercising a particular function of the application. Functional tests typically exercise multiple containers and components to achieve a certain goal condition. Think "what are the steps involved in achieving this result?"
Test files should be named the same as the item being tested with .test
inserted before the file suffix. Unit tests should be located with the item being tested in a special directory named __tests__
.
For example:
The simplest way to run a test is to enter npm test
from anywhere within the src/web
directory tree. This will cause Jest to run all the tests in the directory tree.
Notice in the example below Jest ran all the tests under src/web
even though we were down in the TaskSettingsPanel
folder.
If you want to run the test suites in a particular directory, you just need the name of the directory, you do not need the entire path. In the example below, we are in the src/web
directory and ran all the tests under src/web/containers/TaskSettingsPanel
. It ran 3 test suites (files) from that directory.
If you want to run a single Test Suite you can provide the entire path to the file containing the Test Suite.
You can debug your tests right in VSCode
. Simply insert breakpoints where you need them and click on the Debug
button or Ctrl + Shift + D
to debug the current file.
Testing a React Component is broken down to a basic set of steps.
For a Component test to function properly several things must be considered. We are trying to test a specific piece of the application in isolation, however it needs some context to operate properly.
You will need to determine the minimum set of Prop parameters for the Component to function properly. This will depend on the specific functionality being tested. Props can be declared statically, created dynamically in the test, or read in from files. Typically, you will need to provide any data needed to put the Component in the correct state and callback functions to verify results and support the component behavior.
The word mock should be used when declaring items used to stand in for things outside of the unit(s) being tested. Mocks come in many flavors. You can mock variables, methods, and entire modules as needed (more on that later).
Tests are objects that include a test method that get run using the tools from Jest and RTL. The keyword test
or it
is used to declare a test object. The attributes of a test are a description followed by a test method and then an optional timeout value. Tests methods should be declared using Arrow Function syntax.
Example of complete simple test
RTL uses the jsdom
to simulate rendering of components just as the usual DOM would render them in a browser. The jsdom
includes special hooks and methods to support testing.
Unlike the usual DOM, jsdom
does not automatically re-render components based on changes to Props or Context. Most of our components are "controlled components", meaning their state is controlled by props and functionality is provided by callback functions. This means we need to explicitly re-render a component under test to cause changes to it. The render
method of RTL returns a reference to the rendered object which includes a rerender
method.
RTL provides many different ways to query the rendered component to get access to a specific element. See About Queries.
The object screen
represents the rendered object being tested. The most common queries are listed below. In general, these use attributes of the element already existing in the code.
In general, it is best to query using something visible to the user to make tests more consistent with how a user would use the application. In some cases, this can be difficult or impossible. In those cases, we can use a special attribute called data-testid
. This is generally considered a last resort since this attribute has no corresponding use in the real application.
Example Button:
These methods all return a reference to the same Button object:
Once you have access to an element, you will need to trigger it to make the component do something. There are two ways to trigger an element:
In general, we always want to use the methods in the user-event
companion library. fireEvent
creates DOM events directly. user-event
more closely emulates user interactions in a browser, which may trigger more than one DOM Event.
Triggering the Button element described above:
Many aspects of React applications run asynchronously so browsers do not lock up waiting for something to change. In general, anything that returns a promise is asyncrhonous. For this reason, we want to declare most of our tests as aynchronous methods.
And because things are asynchronous, we may have to wait for things to happen before continueing the test. See Waiting for appearance.
This is why we have await
in front of the call to userEvent.click
above. This allows the test to wait until the promise is fulfilled before going further. In some cases we need to wrap the method with waitfor
as well.
This may take some trial and error to get right at first. Here are 2 discussions on the subject of asynchronous testing.
We often want to test that the visual elements of our components behave correctly. In general, we should always verify certain attributes of an element we expect to change from a trigger event. We may also want to verify elements are displayed correctly after rendering. Jest provides an expect
method for making assertions. (Note: assert
from Node.js
can be used if needed but is not recommended)
Assert an element renders with the correct value:
Assert an element changes after re-rendering:
Other examples:
As stated above, most of our components are controlled, therefore, the outputs of them are passed as arguments to callback functions provided in the Props. In general, we create mock callback functions for our tests. These can be declared in the test object itself, outside the test object, or in a different file. It depends on the scope and reusability of the mock callback as to which location makes the most sense.
Jest provides a convenient function for declaring mocks that can be overloaded as needed. The benefit of declaring your mocks as jest.fn
is Jest provides a bunch of helper functions and attributes that can be used to verify how the callback has been used.
When declaring a mock using jest.fn
, you can also provide some mock implementation.
Modifying Props in a mock callback example:
In some cases, you may want to create a wrapper container around the component that can maintain state:
Becasue most of our components are controlled by their parents, one way to verify data is to check the Props after they have been modified by the callback function. This would typicaly be done with a rerender
call when you are also checking changes to the elements. You can do this directly in the test or in a separate 'helper' method.
Here we are checking an updated task
Prop has been created correctly:
A very common use of mocks is to replace things that use a lot of resources or exercise external interfaces that are not needed for the test. This makes the tests run faster and avoids potential issues timing out waiting for external events.
We are still evolving in our use of mocks. Detailed informaiton is available here Jest Mock Functions. Please update this section if you learn a new way of using mocks.
CommandControl.tsx
imports and uses CustomLayerGroupFactory
to create map layers that require data downloaded from the Hub's server at run time. We do not need these for our tests and do not want our tests to depend on data being downloaded from an external source. We don't want to modify the source code being tested, so we create a mock to take the place of CustomLayerGroupFactory
. General mocks like this should be placed in the src/web/tests/__mocks__/
directory.
src/web/tests/__mocks__/customLayers.mock.ts
Here we create a mock of the CustomLayerGroupFactory class and the methods needed to support our test:
Using the mock in src/web/containers/CommandControl/__tests__/CommandControl.test.tsx
:
Sometimes we want to mock part of a module in our test to avoid costly operations that are not needed for the test but do not need or want to mock the entire module. In this case, we need only a partial mock of the module.
The JaiaAPI
is a good example of this. The JaiaAPI
class includes many methods used throughout our code. However, all external communication is handled by the hit
method. Rather than mocking every method in the class and trying to figure out what implementation may be needed for each one we simply replace the hit
method with a mock and use the rest of the real JaiaAPI
class. We use the jest.requireActual
function to achieve this.
src/web/tests/__mocks__/jaiaAPI.mock.ts
Here we tell Jest we want to use the real JaiaAPI
class from src/web/utils/jaia-api.ts
but replace its hit
method with a mock that returns a mocked response.
A file with Jest tests in it is considered a "Test Suite". A single test
or it
declares a "Test". Tests within a Test Suite can be further grouped by wrapping them with a describe
block. It is important to keep the scope of these blocks in mind when declaring tests or items to support your tests.
Often when running multiple tests and groups of tests we need to run some code before and/or after each test or group to put the system in the correct state for each run. This is typically referred to as "Setup" and "Teardown" in testing. Mocks in particular often need to be reset as well as Props used in tests as part of a Setup. Teardown can be used to free up resources allocated during a test (sockets, memory, etc), reset state, or anything else that has no use after a test is run. It is important to remember that you can not count on Jest running your tests in any particular order, so do not rely on the end state of one test as the starting state of another test.
Jest uses beforeAll()
, beforeEach()
for setup and afterAll()
and afterEach()
for teardown.
beforeAll()
and afterAll()
are run once, before and after all the files in a particular block scope (either entire file, a describe block, or a single test). beforeEach()
and afterEach()
are run before and after each individual test in a block.
Often we want to run the same test code over and over with different data. Rather than copy/paste a test and changing a few items, look for ways to extract what is different into parameters, just as you would if you were going to turn a block of code into a more general function. A set of parameters used for a particular test run is typically refered to as a "Test Case". Jest provides a test.each() wrapper method to achieve this. The parameters are an array and each element can contain as much data as needed.
In the example below, we define our parameters to include a string
to help identify the test case and a MissionTask
to be used for the test run. We will put the test cases in an array named validTaskTestCases
to pass to our parameterized test. Because there was a large number of different MissionTask
objects to be tested, we put them in a json
file to make the code cleaner and easier to add, modify, or delete test cases. (This same file of test cases is used in another test suite and contains both validTaskTestCases
and a set of invalidTaskTestCases
. For this test suite, we are only using the validTaskTestCases
.)
Next we declare our parameterized test. We use the beforeEach()
method in a describe
block to reset the Props used and to reset all mock data. We use test.each(validTaskTestCases)
to tell Jest to run this test with each test case in the validTaskTestCases
. We use the description
of the test case to augment the test description to make the test output more useful and then pass the task
of the test case into the test function as a parameter and add it to the default Props for our test.
Here is the output from the parameterized test:
Our testing environment includes a lot of individual tools working together, each with their own configurations. Here we will discuss these but only focus on the test related configuration parameters.
Automatically reset the data associated with mock calls so it does not need to be done explicitly in a beforeEach()
method
Tell Jest to use babel
for test coverage instrumentation. Currently we are not accessing any test coverage data.
Tell Jest to use file-mock.ts
to mock imports of image files and style-mock.ts
to mock imports of css
and less
files
Tell Jest to ignore the files in the dist
directory
Tell Jest to use ts-jest as the preset for translating Typescript
Tell Jest to run mocks for Web APIs and set global imports for .test
files
Tell Jest we want to use the jsdom
for our testing environment
Tell Jest to use ts-jest
to translate .ts
& .tsx
files and to use babel-jest
to translate .js
& .jsx
files.
This parameter must be present. It tells Jest to ignore some files. We are not ignoreing any at this time.
Runtime parameters for tests. We increased the testTimeout
and reduced the maxWorkers
to insure our tests can run in CircleCI
. You may want to change these locally if you have a faster or slower machine.
Configurations set on the global object in the JavaScript environment
Tell Typescript to use the types in @testing-library/jest-dom
"types": ["@testing-library/jest-dom", "@types/plotly.js"],
Tell Typescript to exclude our test files from our application code base
exclude": ["node_modules", "dist", "coverage", "webpack.*.js", "*.config.js", "*.test.ts*"]