Skip to main content

Testing Code

We test the Chemotion ELN using three different kinds of tests.

1. JavaScript unit tests (mocha)

npm test

2. Ruby unit tests (RSpec)

RAILS_ENV=test bundle exec rake db:test:prepare && bundle exec rspec --exclude-pattern spec/features/**/*_spec.rb

3. End-to-end (feature) tests (Cypress)

Introduction

cypress-on-rails integrates the Cypress testing framework with Ruby on Rails applications. The gem provides a simple and efficient way to run Cypress tests in a Rails environment, allowing developers to write and execute end-to-end tests.

For more details, please follow official documentation.

Installation

Add this to your Gem file

group :test, :development do
gem 'cypress-on-rails', '~> 1.0'
end

and follow the installation instructions. Note that you don't have to install Cypress separately with yarn or npm. The cypress-on-rails gem will take care of that.

It's important to tell cypress-on-rails to put the project under spec alongside our existing tests. Otherwise the Cypress related files (i.e., cypress directory and cypress.config.js) live under root by default. You can use the following command from Cypress docs to specify the spec directory:

bin/rails g cypress_on_rails:install --cypress_folder=spec/cypress

Usage

Make sure to start the Rails server prior to starting Cypress. Note that it's important to start the server with RAILS_ENV-test, Cypress won't recognize the Rails server if it's running in the development or production env; see here for more.

RAILS_ENV=test $(which bundle) exec rails server -b 0.0.0.0 -p 3000

Subsequently, use another tab to run the following command.

yarn cypress open --project spec

Note that the Cypress app might take some time to start (up to a minute). When you run Cypress from the VSCode devcontainer the Cypress app / interface will open automatically in a new window.

To run all tests, use the following command

yarn cypress run --config-file spec/cypress/cypress.config.js
# or alternatively
yarn cypress run --project spec

Write your first test

Follow the instructions here to write your first end-to-end test.

Example of using factory bot

It is always a good idea to set up testing states programmatically, such that further tests can re-use the setup code. In this regard, factory bots can save you a lot of time. We are using the concept of factory bots as they are originally defined at app/specs/features. From there, we can directly use them or customize them according to our requirements. The example below will create a new user in the database, who is ready to be signed in with the provided credentials.

describe('create user', () => {
beforeEach(() => {
cy.appFactories([
['create', 'user', {
first_name: 'User',
last_name: 'Complat',
password: 'user_password',
password_confirmation: 'user_password',
email: 'cu1@complat.edu',
name_abbreviation: 'cu1',
account_active: 'true',
}],
cy.visit('users/sign_in');
});

Commands in Support Folder

There are many cases where you need to repeat chunks of code. In that case, it is better to use commands. You can call commands wherever they are needed. It saves time and enhances clarity. For example, move the code for the creation of a user to spec/cypress/support/commands.js as shown below:

Cypress.Commands.add("createDefaultUser", (emailAddress, abbr) => {
cy.appFactories([
[
"create",
"user",
{
first_name: "User",
last_name: "Complat",
password: "user_password",
password_confirmation: "user_password",
email: emailAddress,
name_abbreviation: abbr,
account_active: "true",
},
],
]);
});

Then your test will look like this. Notice, how your code became much more readable.

describe('create user', () => {
beforeEach(() => {
cy.createDefaultUser('cu1@complat.edu', 'cu1')
});

Example of user with collection

Here, we are creating a user with an empty collection. then enables you to work with the command's result.

describe('create user', () => {
beforeEach(() => {
cy.createDefaultUser('cu1@complat.edu', 'cu1').then((user) => {
cy.appFactories([['create', 'collection', { user_id: user[0].id }]]);
});
});

Example of creating a sample

One way to create a sample is to create it by automating all the steps that are required by a user to perform in the browser. (Notice, the before block already including the creation of a user and collection).

it("create samples", () => {
cy.visit("users/sign_in");
cy.get("#user_login").type("cu1");
cy.get("#user_password").type("user_password");
cy.get("div").find('[id="tree-id-Collection 1"]').click();
cy.get("#create-split-button").click();
cy.get("#create-sample-button").click();
cy.get(".chem-identifiers-section > .list-group-item").click();
cy.get("#smilesInput").type("c1cc(cc(c1)c1ccccc1)c1ccccc1");
cy.get("#smile-create-molecule").click();
cy.get("#submit-sample-btn").click();
cy.url().should("include", "/sample/1");
});

Example of creating a sample state

cy.createDefaultUser("cu1@complat.edu", "cu1").then((user) => {
cy.appFactories([["create", "collection", { user_id: user[0].id }]]).then(
(collection) => {
cy.appFactories([
["create", "molecule", { molecular_weight: 171.03448 }],
]).then((molecule) => {
cy.appFactories([
[
"create",
"sample",
{
name: "Material",
target_amount_value: 7.15,
molecule_id: molecule[0].id,
collection_ids: collection[0].id,
},
],
]);
});
}
);
});

Example of testing an API endpoint with Cypress

You can also use Cypress to test APIs in a developer-friendly way. You can conveniently access the API response data from the test runner. When you click on the request step, Cypress will display the corresponding response data in the browser's console.

Consider the following example, where we are testing Collection API (GET request).

cy.request("/api/v1/collections/roots.json").as("roots");
cy.get("@roots").then((response) => {
expect(response.status).to.eq(200);
const collectionData = response.body["collections"][0];
expect(collectionData.id).to.eq(3);
expect(collectionData.label).to.eq("Col1");
});

Here, were are sending a GET request to the api/v1/collections/roots.json endpoint and verify that its response.status is 200. The response body can contain objects, arrays, attributes, etc. This is how we can access a specific component from its body and perform checks on it.

// whole object
const collectionData = response.body["collections"];

// single item from the object list
const collectionData = response.body["collections"][0];

// check length of object
expect(response.body["collections"].length).to.eq(0);

// check if returned fields are matching exactly what's expected
expect(collectionData.id).to.eq(3);
expect(collectionData.label).to.eq("Col1");

General rules of thumb

  • All public methods should be tested, every statement, conditional branch should be covered.
  • Tests should be easy to read and understand. Try to use a maximum of 3 describe/context levels:
    • Describe: What is the class/method I want to test?
    • Context: Under what circumstances is the method tested?
    • It: What do I expect?
  • Verifying UI state should not be the focus of unit testing (with the exception of snapshot testing in JavaScript).
  • Useful resources

Javascript Unit tests

  • Test files should be placed in spec/javascript/..., mirroring the location of the source file (i.e., the file under test).
  • Test files should have the name of the file under test but end with .spec.js.
  • For simple plain JavaScript objects, test every "public" method. Without access modifiers, look what methods are called by other components.

Testing React components

  • Use the Enzyme library to create React components: wrapper = shallow(<CLASS_OF_COMPONENT/>);
  • React components can be used with their JavaScript-state or their HTML representation: wrapper.instance() and wrapper.html()
  • Example: ResearchPlanDetailsFieldImage.spec.js
Important

When testing a component which includes a store you need to explicitly import he store in the test file. The import must be before the import statement of the component to test. The linter recognizes that the store dependency is not used and tries to eliminate it. You can prevent this by manually disabling the linter rule for these lines. Example: ResearchPlanDetails.spec.js

Stubbing/Mocking

Creating models with factories

  • To create model objects, we use the library factory-bot
  • const researchPlan = await ResearchPlanFactory.build('empty');
  • By using a pseudoRandomGenerator in the factory for ids and checksums we can create reproducible objects
  • One can reset the generator by adding a build option: const researchPlan = await ResearchPlanFactory.build('empty', {}, { reset: true });
Important

Creating a model is an asynchronous action, so the test must also be asynchronous: it('no img tag should be rendered', async function (){ }

Snapshot testing

Deprecated chapter

Currently no snapshot testing is applied due to lack of capacity. In addition, ui testing will be covered by the system tests in the future.

  • When testing a React component, we want to verify the state of the component. An easy way to achieve this would be to check if its HTML state is as expected.
  • By using the snapshot library expect-mocha-snapshot, a snapshot of the HTML state is generated at the first test run. After that, the state in the test is checked against the saved state from the first run.
  • Snapshots are saved in a folder snapshots at the same level of the test
  • Snapshots are not only restricted to HTML but can also be used for JSON or JavaScript-objects
  • Example: ResearchPlanDetailsFieldImage.spec.js
Important

The expect extension by mocha refers to the current test-object by this. Using an lambda expression at the it definition, mocha have no access to it. Using an explicit function definition solves this problem.

Ruby Unit tests

Common infos

  • Tests should reflect the structure of the source package. That means having the exact same location and name ending with _spec.rb.
  • Example test

Creating database models with factory bot

  • Models can be created using the library factory bot. In factories, desired states of the models are predefined and can then be created using create (creates a DB entry) or build (creates a model without a DB entry).
Important

Creating an attachment with factorybot must be handled carefully because create also triggers the create_derivative method and could remove a fixture file. By using build the create_derivative method is not executed, but one has no object in the database and some properties are not initialized.

Testing methods without classes

  • can be done by adding type :helpers Rspec.describe GenericHelpers, type: :helper do. Example generic_helpers_spec.rb

Stubbing/Mocking

  • REST calls stub_request(REST_METHOD_AS_SYMBOL, 'REST_ADDRESS' .with(REQUEST_HEADER_INFOS OR/AND PARAMETER) .to_return(RETURN BODY OR/AND HEADER) Example: spec_helper.rb
Info

Many REST requests that request INCHI keys are already defined in spec_helper.rb

  • Methods of classes/objects allow_any_instance_of(CLASS_NAME).to receive(METHOD_NAME_AS_SYMBOL).and_return(RESULT) allow_any_instance_of(CLASS_NAME).to receive(METHOD_NAME_AS_SYMBOL).and_raise(AN ERROR)
  • for more infos: rspec-mocks

API - testing

  • Tests for the api have to be in the subfolder spec/api/... to work properly

Acceptance tests

When you have freshly installed Chemotion ELN, make sure to create a welcome-message.md file in the public directory inside Chemotion ELN before running acceptance tests. You can create it from the example file:

Coverage

Infos about code coverage: Code Coverage

JavaScript unit tests

  • Using npm run coverage creates a coverage report in HTML written in .coverage. The method of covering is condition coverage.
  • in package.json one can exclude directories from coverage
  • Coverage is currently not included in CI

Rspec unit tests

By running the rspec unit tests, the coverage is currently calculated automatically. The results can be found in the folder .coverage