Testing Requirements
The basics
When changing code in an Open Commerce repository, you should test your code before submitting a pull request for review. Open Commerce testing breaks down into three broad categories:
Unit tests
Integration tests
Acceptance tests
All unit and integration tests should use the Jest framework. When submitting a pull request, CircleCI automatically runs unit and integration tests and will not allow merging until they all pass. Acceptance tests verify app functionality from a user’s perspective and can be done with a script or manually.
In this documentation, we’ll cover how to create and run unit and integration tests so your code can be accepted into the Open Commerce codebase.
Unit vs integration tests
Because many Open Commerce services integrate with others, there’s no sharp divide between unit and integration tests. Think of the terms “unit” and “integration” as two ends of a spectrum, where the “unit” end mocks everything and tests in complete isolation, while the “integration” end mocks nothing and is essentially like running the Open Commerce app itself.
In practice, the primary distinction is:
Unit tests have no mock database and are written in files throughout the codebase, with names similar to the name of the file they test.
Integration tests are all written in the
/tests
folder and can make use of a fake, in-memory version of MongoDB.
For example, say you’ve written GraphQL resolvers for shop
and tags
, and you want the two resolvers to work together to produce a shop.tags
field. You’ll need to write unit tests for each of them to determine whether they’re returning the correct information—the shop or tags list, respectively. But each individual unit test will ignore or mock the output of the other, so you’ll need an integration test to ensure that the output of shop.tags
is correct.
Writing Jest unit tests
Every component and utility function in Open Commerce must have a corresponding file containing unit tests written using the Jest testing framework. If you aren’t familiar with Jest, you should check out the Jest documentation before writing your tests.
Some older files may not have existing unit tests. In order to update them, you must create a unit test file and add all necessary tests to achieve full coverage of that file—including testing all existing code as well as your new changes. It’s often easiest to begin your development by writing all of the missing tests before changing any code.
Filenames and code style
Jest unit test files must be named after the component or utility function that they are testing and end in .test.js
. The test file can be anywhere in the Open Commerce codebase, but ideally it should be in the same folder and have the same base filename as the code being tested.
When writing your test:
Do not add extra
describe
blocks. Jest tests are automatically file-scoped, so a file that performs a single test does not need adescribe
block in it. You may add multipledescribe
blocks to group related tests within one file, but you should not have a file with only a single
describe
block in it.Always use
test()
instead ofit()
to define test functions.Do not import
describe
,test
,jest
,jasmine
, orexpect
. They are automatic globals in all test files.Use arrow functions for all
describe
andtest
functions.Use Jest’s built-in
expect
function for assertions.
You might need to test asynchronous code: functions that either return a Promise or take a callback argument. You should use Promises unless you need to use a callback, such as when the API of another package requires it. When using callbacks, make sure to add a done
argument to your test function and call done
when all testing is complete.
Mocking data
To write unit tests for modules that depend on other modules, you need to supply mock data.
There are several ways to mock data in Open Commerce:
Use the rewire-exports Babel plugin, which can temporarily replace anything that another file exports. This is useful for mocking functions that are imported by the function that you’re testing.
Use the built-in
Factory
test utility, which uses@reactioncommerce/data-factory
to attach all core schemas to theFactory
object. You need to add the schema to theFactory
prior to use, or it will evaluate to an empty object.
Note: Factory
should primarily be used for backend-specific testing, such as integration testing at the API server level or unit testing at the plugin level.
To set up the initial schema for Factory
, use SimpleSchema
and createFactoryForSchema
, like this:
Initial schema for Factory
import SimpleSchema from 'simpl-schema';
import { createFactoryForSchema } from "@reactioncommerce/data-factory";
const Example = new SimpleSchema({
strProp: String,
boolProp: Boolean,
});
createFactoryForSchema("Example", Example);
To mock a single object, use the makeOne()
method with the name of the collection, such as:
Mock a single object
import { Factory } from "/imports/test-utils/helpers/factory";
const mockTag = Factory.Tag.makeOne();
The output of Factory
will be something like:
Factory output
{
_id: "e02993ea96d7",
name: "mockName",
slug: "mockSlug",
type: "mockType",
metafields: ["item"],
position: 3ff4e0634ecc,
relatedTagIds: ["mockRelatedTagIds.$"],
isDeleted: false,
isTopLevel: true,
isVisible: true,
groups: ["mockGroups.$"],
shopId: "a05276973251",
createdAt: "1970-01-02T02:28:37.000Z",
updatedAt: "2020-03-01T19:16:58.117Z",
heroMediaUrl: "mockHeroMediaUrl"
}
To mock multiple instances of a single type of object, use the makeMany
method with an integer argument:
Mock multiple objects
import { Factory } from "/imports/test-utils/helpers/factory";
const mockTags = Factory.Tag.makeMany(2);
makeMany
will output an array of objects:
makeMany output
[
{
_id: "e02993ea96d7",
name: "mockName",
slug: "mockSlug",
type: "mockType",
metafields: ["item"],
position: "3ff4e0634ecc",
relatedTagIds: ["mockRelatedTagIds.$"],
isDeleted: false,
isTopLevel: true,
isVisible: true,
groups: ["mockGroups.$"],
shopId: "a05276973251",
createdAt: "1970-01-02T02:28:37.000Z",
updatedAt: "2018-06-04T19:16:58.117Z",
heroMediaUrl: "mockHeroMediaUrl"
},
{
_id: "bdc84075a8eb",
name: "mockName",
slug: "mockSlug",
type: "mockType",
metafields: ["item"],
position: "5034c879b7c2",
relatedTagIds: ["mockRelatedTagIds.$"],
isDeleted: false,
isTopLevel: true,
isVisible: true,
groups: ["mockGroups.$"],
shopId: "28d65013adc8",
createdAt: "1970-01-02T02:28:37.000Z",
updatedAt: "2018-06-04T19:16:58.117Z",
heroMediaUrl: "mockHeroMediaUrl"
}
]
You might need to specify a value for a property within your mock data, like making all mocked objects have the same shopId
. Provide a properties object as an argument to either makeOne
or makeMany
:
Specify values in mocked objects
import { Factory } from "/imports/test-utils/helpers/factory";
const mockTag = Factory.Tag.makeOne({ shopId: "1234" });
makeMany
also supports custom values that are not constant, e.g., a series of mock objects that have sequential _id
values. To use an arrow function to output two mocked tags with _id
values of "100"
and "101"
:
Sequential values in mocked objects
import { Factory } from "/imports/test-utils/helpers/factory";
const mockTags = Factory.Tag.makeMany(2, { shopId: "1234", _id: (index) => (index + 100).toString() });
Finally, indexes can be passed from a makeMany
method into another method. Here, passing 30
into makeMany
will also pass 30
into makeMockProductWithSpecificId
:
Passing indexes
function makeMockProductWithSpecificId(index) {
const productId = (index + 100).toString();
return Factory.CatalogProduct.makeOne({
_id: productId,
isDeleted: false,
isVisible: true,
tagIds: [mockTagWithFeatured._id],
shopId: internalShopId
});
}
const mockCatalogItemsWithFeaturedProducts = Factory.Catalog.makeMany(30, {
product: makeMockProductWithSpecificId,
shopId: internalShopId
});
React component tests
Many Open Commerce user interface components are written in React. To write Jest unit tests for these components, you need to use a tool that renders them for testing purposes. We recommend the React Testing Library, which allows writing tests that closely resemble how interface components are used, rather than their inner workings.
Writing Jest integration tests
Jest integration test files always end in .test.js
and should be located in the /tests
folder. Integration tests can write some initial test data to an in-memory MongoDB store, allowing them to test database queries without mocking them. The MongoDB collections are simulated in-memory collections, implemented using mongodb-memory-server
.
GraphQL integration tests
Integration tests can send actual GraphQL requests to a temporary test server that behaves like the real server. This makes them useful for testing things such as:
Queries involving multiple resolvers
Response pagination
Whether mutations properly affect the MongoDB collections
The effect of complex permission rules on query results
Note: The query
, mutation
, and subscription
properties of the test server are wrappers around the methods of the same name provided by the graphql.js package.
When creating GraphQL tests, the folder and file structure in /tests
should match the plugin folder structure as much as possible.
Prior to running the tests in each file, initialize a server, in-memory database, and any collection data you need; then stop the server, like so:
Integration test setup
import { importPluginsJSONFile, ReactionTestAPICore } from "@reactioncommerce/api-core";
let testApp
beforeAll(async () => {
// init the reaction test API server
testApp = new ReactionTestAPICore();
// create a list of plugins
const plugins = await importPluginsJSONFile("../../../../../plugins.json", (pluginList) => {
// Remove the `files` plugin when testing. Avoids lots of errors.
delete pluginList.files;
return pluginList;
});
// register plugins and start test server
await testApp.reactionNodeApp.registerPlugins(plugins);
await testApp.start();
})
// stoping the test server will drop all test data
afterAll(() => testApp.stop());
test("primaryShop query returns a shop", async () => {
// (1) use testApp.collections to write to MongoDB collections to set up initial data state
await testApp.collections.Shops.insertOne({ shopType: "primary", name: "Test Shop", createdAt: new Date(), _id: "123456" });
// (2) execute a GraphQL query or mutation using testApp.query()() or testApp.mutation()()
const results = await testApp.query(`query primaryShop { name _id }`);
// (3) verify the response is as expected and/or verify that the collection data has been changed
expect(results.name).toBe("Test Shop");
// (4) optionally reset the database if there is a chance it will conflict with the next test in this file
await testApp.collections.Shops.remove({ _id: "123456" });
});
The following code snippets are examples of common tasks to include in GraphQL integration tests. mockAccount
may either be an account document that you’ve already inserted or one that the test server will insert for you. Either way, it must have an _id
property, which will be used to set the user
and account
properties of the GraphQL context for all test queries.
To insert a primary shop:
Insert primary shop
const shopId = await testApp.insertPrimaryShop();
To create a mock user:
Create mock user
mockAccount = Factory.Accounts.makeOne({
// ...any specific properties you need on the account
});
await testApp.createUserAndAccount(mockAccount);
To create a mock admin user:
Create mock admin
mockAdminAccount = Factory.Accounts.makeOne({
// ...any specific properties you need on the account
});
await testApp.createUserAndAccount(mockAdminAccount, ["owner"]);
To set and clear the mock user:
Set and clear mock user
beforeAll(async () => {
await testApp.setLoggedInUser(mockAccount);
});
afterAll(async () => {
await testApp.clearLoggedInUser();
});
Running Jest tests
When running tests, first make sure they are located in the correct directory (for unit tests, the same directory as the code being tested; for integration tests, the /tests
folder).
To run tests, use npm
and specify the type of test being run—either npm run test:unit
or npm run test:integration
. Most repos will also accept npm run test
for running unit tests. To have tests rerun as you make changes to test files, add :watch
—either npm run test:unit:watch
or npm run test:integration:watch
.
Note: To use watch mode on macOS, you must install watchman
. This can be done via the Homebrew package manager by running brew install watchman
.
Jest has a built-in caching feature, which can sometimes give you bad cached results, even if the test has since been fixed. To force a test to ignore the cache, add the --no-cache
flag.
You can also use Docker Compose to run tests within a local development container. This gives a more accurate picture of how production code running in a container will behave. Run docker-compose run --rm reaction npm run test:unit
or docker-compose run --rm reaction npm run test:integration
These tests can also be run in watch mode by suffixing :watch
.