Categories
Testing

Getting Started with Testing with Jasmine

Testing is an important part of JavaScript.

In this article, we’ll look at getting started with Jasmine.

Using Jasmine with Node

We can add Jasmine to our Node project by running:

npm install --save-dev jasmine

This will install a local copy of Jasmine for our project.

We can also install it globally by running:

npm install -g jasmine

Then we can initialize the Jasmine project by running:

jasmine init

if we installed Jasmine globally.

If we didn’t we run:

npx jasmine init

Then we generate the spec and source files by running:

jasmine examples

Then we can change the configuration in spec/support/jasmine.json /

The spec_dir property is the directory with the test files.

spec_file has the patterns for the spec files.

helpers have the paths for the helpers for the test files.

stopSpecOnExpectationFailure is a boolean that indicates whether we want to stop running tests when a test fails.

random means whether we run specs in a semi-random order.

We can run tests with:

jasmine spec/appSpec.js

where spec/appSpec.js is the path.

We can also specify a pattern for the path of the tests;

jasmine "**/tests/**/*Spec.js"

CLI Options

We can change the options by setting some config.

For instance, we can write:

JASMINE_CONFIG_PATH=spec/config/jasmine.json jasmine

jasmine --config=spec/config/jasmine.json

We can disable colors with the --no-color flag.

--filter lets us specs that match a given string.

--stop-on-failure lets us stop running tests on first failure if it’s true .

--random tells Jasmine to run tests in semi-random order.

--seed lets us set the randomization seed if randomization is turned on.

--reporter lets us set the test reporter.

We can also use the jasmine library to set the config.

For instance, we can write:

const Jasmine = require('jasmine');
const jasmine = new Jasmine();
jasmine.loadConfigFile('spec/support/jasmine.json');
jasmine.loadConfig({
  spec_dir: 'spec',
  spec_files: [
    'appSpec.js',
    'requests/**/*[sS]pec.js',
    'utils/**/*[sS]pec.js'
  ],
  helpers: [
    'helpers/**/*.js'
  ]
});

to set the config.

We can also set our own onComplete callback to do what we want is all tests pass or when they fail:

jasmine.onComplete(function(passed) {
  if (passed) {
    console.log('passed');
  } else {
    console.log('failed');
  }
});

Reporters

We can set the test reports.

The default is the ConsoleReporter.

We can set the reporter by writing:

jasmine.configureDefaultReporter({
  timer: new jasmine.jasmine.Timer(),
  print(...args) {
    process.stdout.write(args);
  },
  showColors: true
});

to change how test results are reported.

Run Tests

To run tests, we run the execute method:

jasmine.execute();

We can also pass in a file name or spec name to run them:

jasmine.execute(['fooSpec.js'], 'a spec name');

So we can write:

const Jasmine = require('jasmine');
const jasmine = new Jasmine();

jasmine.loadConfigFile('spec/support/jasmine.json');
jasmine.configureDefaultReporter({
  showColors: false
});
jasmine.execute();

to run our tests.

Conclusion

We can use Jasmine to run our tests.

There’re many options we can set, like the files to run, whether to show colors or not, randomize tests, etc.

Categories
Testing

JavaScript Unit Test Best Practices — UI Tests

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Query HTML Elements Based on Attributes that are Unlikely to Change

We should only check for HTML elements with attributes that aren’t likely to change.

This way, we don’t have to update our tests when we make small changes.

Also, this ensures that look and feel changes don’t break our tests.

For example, instead of writing:

test("whenever no data is passed, number of messages shows zero", () => {
  // ...
  expect(wrapper.find("[className='mr-2']").text()).toBe("0");
});

to test:

<span id="metric" className="mr-2">{value}</span>

Instead, we add our own ID to the element that we want to test and then use that in our test.

For instance, if we have:

<h3>
  <Badge pill className="fixed_badge" variant="dark">
    <span data-testid="msgs-label">{value}</span>
  </Badge>
</h3>

We can test it with:

test("whenever no data is passed, number of messages shows zero", () => {
  const metricValue = undefined;
  const { getByTestId } = render(<dashboardMetric value={undefined} />);
  expect(getByTestId("msgs-label").text()).toBe("0");
});

We shouldn’t rely on CSS attributes that can change at any time.

Instead, we add an ID that rarely or never changes.

Test with a Realistic and Fully Rendered Component

We should test with realistic and fully rendered components.

This way, we can trust our test that it’s actually testing the stuff in the component.

If we mock or do partial or shallow rendering, we may miss things in our tests.

If it’s too slow to test with the real thing, then we can consider mocks.

For instance, instead of shallow rendering with shallow :

test("when click to show filters, filters are displated", () => {
  const wrapper = shallow(<Calendar showFilters={false} title="Select Filter" />);
  wrapper
    .find("FiltersPanel")
    .instance()
    .showFilters();

  expect(wrapper.find("Filter").props()).toEqual({ title: "Select Filter" });

});

We write:

test("when click to show filters, filters are displated", () => {
  const wrapper = mount(<Calendar showFilters={false} title="Select Filter" />);
  wrapper.find("button").simulate("click");
  expect(wrapper.text().includes("Select Filter"));
});

We call mount to mount the Calendar component fully.

Then we do the click on the button as we do like a real user.

Then we check the text that should appear.

Use Frameworks Built-in Support for Async Events

We should test frameworks built-in async events when we’re running our tests.

This way, we actually wait for what we want to appear before running something.

Sleeping for a fixed time isn’t reliable and doesn’t help with waiting for items to appear before doing what we want.

This means our tests would be flaky.

Also, sleeping for a fixed time is a lot slower.

For instance, with Cypress, we can write:

cy.get("#show-orders").click();
cy.wait("@orders");

We wait for orders to appear when we click on the element with ID show-orders .

What we don’t want is to have code that waits with our own logic with setInterval :

test("user name appears", async () => {
  //...
  const interval = setInterval(() => {
    const found = getByText("james");
    if (found) {
      clearInterval(interval);
      expect(getByText("james")).toBeInTheDocument();
    }
  }, 100);

  const movie = await waitForElement(() => getByText("james"));
});

This is complex and we don’t take advantage of the full capabilities of test frameworks.

Conclusion

We should wait for things with test frameworks’ wait functions.

Also, we should test with realistic components.

Categories
Testing

JavaScript Unit Test Best Practices — Testing Behavior

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Test the Behavior and not the Internal Implementation

We should only testy the result and not worry about the internal implementation.

This way, we won’t worry ourselves with something that doesn’t need to be checked in tests.

For instance, we shouldn’t test internal variables:

it('should add a user to database', () => {
  userManager.addUser('james', 'password');

  expect(userManager._users[0].name).toBe('james');
  expect(userManager._users[0].password).toBe('password');
});

Instead, we write:

it('should add a user to database', () => {
  userManager.addUser('james', 'password');
  expect(userManager.login('james', 'password')).toBe(true);
});

We just test the returned results instead of internal variables.

This way, we don’t have to change our tests when the implementation changes.

Don’t Mock Everything

We shouldn’t mock everything.

This way, at least we’re testing something.

For instance, we can write:

describe('when the user has already visited the page', () => {
  describe('when the message is available', () => {
    it('should display the message', () => {
      const storage = new MemoryStorage();
      storage.setItem('page-visited', '1');
      const messageManager = new MessageManager(storage);
      spyOn(messageManager, 'display');
      messageManager.start();
      expect(messageManager.display).toHaveBeenCalled();
    });
  });
});

We use a memory storage solution instead of real local storage.

This way, our test doesn’t commit any side effects with our test.

We didn’t mock messageManager.display since we just want to check it’s called.

We use real versions of objects if it’s simple to set up.

They also shouldn’t create shared states between tests.

The speed of the real object should be fast if it’s used.

The real object also shouldn’t make any network requests or browser page reloads.

Create New Tests for Every Defect

There should be new tests for all defects that are fixed.

This way, we can fix it and never have it appear again in the same form.

Don’t Write Unit Tests for Complex User Interactions

Unit tests should be used to test simple actions.

If we have more complex workflows we want to test, then we should add integration or end to end tests.

They’re all needed for more complex workflows like filling forms and submitting data, etc.

Functional tests can be written with frameworks like Selenium or Cypress.

Test Simple User Actions

We should test simple user actions like clicks and inputs.

For example, we can write:

describe('clicking on the "Preview profile" link', () => {
  it('should show the profile preview if it is hidden', () => {
    const button = document.querySelector('button');
    const profileModule = createProfileModule({ visible: false });
    spyOn(profileModule, 'show');
    button.click(previewLink);
    expect(profileModule.show).toHaveBeenCalled();
  });

  it('should hide the profile preview if it is displayed', () => {
    const button = document.querySelector('button');
    const profileModule = createProfileModule({ visible: true });
    spyOn(profileModule, 'hide');
    button.click();
    expect(profileModule.hide).toHaveBeenCalled();
  });
});

We have the profileModule with various states and we do the click on each.

Then we check which function is called.

Review Test Code

Test code should be looked at so that we know the intent of the developer quickly.

Conclusion

We should test simple behavior in our tests.

Also, we shouldn’t mock everything to have more realistic tests.

Categories
Testing

JavaScript Unit Test Best Practices — Test Data and Logic

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Test Middlewares in Isolation

We should test middleware in isolation so that we can test the critical parts of our app.

Middlewares are used by everything, so we should test them.

Since they’re functions, we can test them in isolation.

We can just invoke it directly.

Also, we can use node-mock-http to spy on the behavior of the middleware.

We can write:

const unitUnderTest = require("./middleware");  
const httpMocks = require("node-mocks-http");  
  
test("should return http status 403 when request header is empty", () => {  
  const request = httpMocks.createRequest({  
    method: "GET",  
    url: "/user/1",  
    headers: {  
      authentication: ""  
    }  
  });  
  const response = httpMocks.createResponse();  
  unitUnderTest(request, response);  
  expect(response.statusCode).toBe(403);  
});

We use the node-mocks-http to make our requests.

Then we can pass the request and response objects into our middleware.

And then get the response.

Measure and Refactor Using Static Analysis Tools

We should use static analysis tools to improve our code quality and keep our code maintainable.

It’s useful for detecting duplication, code complexity, and other issues.

We can use tools like Sonarqube and Code Climate to check the statically analyze our app.

Check Our Readiness for Node-Related Chaos

We should also check for issues like when the server or a process dies, or when memory is overloaded.

Speed issues are also important to check.

To regularly free up resources, we can use tools to regularly kill our app instances.

We can do performance testing to find those issues.

APM tools will also help us find them.

Avoid Global Test Fixtures and Seeds

Global test fixtures aren’t good since they’re shared between multiple tests.

We don’t want to share them between multiple tests since we want independent tests.

Tests shouldn’t start with data that have been manipulated by other pieces of code.

Therefore, we should reset the data after each test so that we can always test with clean data.

Our tests should be able to run in any order and still give us the same results.

They also keep our tests simple since they’ll be easier to trace.

For instance, we write something like:

it("should return true if we can log in with valid username and password", async () => {  
  const username = 'james';  
  const password = 'password';  
  await addUser({ username, password });  
  const result = login(username, password);  
  expect(result).to.be(true);  
});

We create a new user and then use that to log in so that we know the user that we’re testing with exists.

Frontend Testing

Front end testing is also important.

Separate UI from Functionality

We should test UI separately from functionality.

When we’re testing component logic, then UI becomes the noise that should be extracted.

This way, our tests can focus on only manipulating data.

UI can be tested separately.

We can disable animation and only check data in tests.

Conclusion

We can separate our logic tests with our UI tests.

Also, we test middleware in isolation.

Data should also be created from scratch for each test.

Categories
Testing

JavaScript Unit Test Best Practices — Test Categories

nit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Tag Our Tests

We should tag our tests so that they can run when commits are being made or builds are being run.

This way, we can, we can do sanity checks before we commit anything by running the most critical tests.

For instance, we can write:

describe("user service", function() {
  describe("log in #sanity", function() {
    test("can log in with the right username and password #sanity", function() {
      //...
    });
  });
});

Categorize Tests Under at Least 2 Levels

We should apply some structure to our test suite so that anyone can understand the requirements.

Categorizing tests will improve the readability of test reports.

We can just go to the desired section and find out the cause of failures.

For example, we can categorize them by writing:

describe("user service", () => {
  describe("log in", () => {
    it("user can log in with right username and password", () => {});

    it("should return error with wrong username or password", () => {});
  });
});

A bit of nesting helps us find our tests easier.

Backend Testing

Our testing portfolio can be improved with tests other than unit tests.

We also look at integration tests and end to end tests to improve our testing portfolio.

3 kinds of tests together will have more comprehensive testing coverage that just unit tests.

There’re many things that unit tests don’t cover like inter-service calls and message queues that don’t get tested with unit tests.

But they can be tested with end to end tests.

We can follow the testing pyramid with lots of unit tests, some integration tests, and a few end to end tests for testing the most critical parts of our app.

Component Testing

Unit tests cover a tiny portion of an app and it’s expensive to cover everything with them.

End to end tests can cover multiple parts of an app but they’re flaky and slower.

A balanced approach would be more appropriate.

We can write things that are bigger than unit tests and smaller than end to end tests.

They can test components, which is a unit.

We can test the whole API without mocking anything that belongs to the microservice itself.

We have the real database, at least an in-memory version.

This can let us gain more confidence in our tests.

We still stub external APIs and other external dependencies.

Ensure New Releases Don’t Break the API

To make sure that we don’t break anything when we release our API to clients, we should test the contract of our API.

This way, we know when we make a request with the given payload, we get the same response that the client expects.

This ensures that we can test our API before clients get their hands on their API.

Conclusion

We can tag our tests to make them run during commits quickly.

Also, we categorize our tests to make them easier to find.

We can also add component tests to make sure we don’t break anything.