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()
andwrapper.html()
- Example: ResearchPlanDetailsFieldImage.spec.js
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
- Heavy dependencies like REST calls can be stubbed/mocked in a test
- We use the library sinon
Sinon.stub(CLASS_TO_MOCK, "METHOD_NAME").callsFake(LAMBDA as replacement of original method);
- Example: ResearchPlanDetailsFieldImage.spec.js
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 });
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
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
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) orbuild
(creates a model without a DB entry).
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
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