Software testing for a developer involves creating and running tests to identify and isolate defects, bugs, or issues in the code, as well as ensure that the software is reliable, robust, and scalable. By performing various types of testing, developers can improve the overall quality and reliability of the software, and ultimately deliver a better product to their users.
Jest is the testing framework for JavaScript. It is good for any JavaScript framework, be it Vue, Angular, React, Meteor.js, Express, jQuery, Laravel, Bootstrap, Node.js, or others. It applies to any kind of testing—client and server, unit and snapshot, and internal and external dependency. This blog is great for software engineers looking to upgrade their skills so they can build better applications in terms of stability and scalability, and it will focus on unit, integration, and snapshot tests only.
We’ll cover:
Welcome to the practical guide to using Jest—a JavaScript testing framework that focuses on simplicity. Quality tests allow for confident and quick iteration, while poorly written tests can provide a false sense of security.
Jest is a test runner that executes tests seamlessly across environments and JavaScript libraries. This blog aims to teach Jest in a framework-agnostic context so that it can be used with different JavaScript frameworks. Jest provides an environment for documenting, organizing, and making assertions in tests while supporting the framework-specific logic that will be tested.
React App Testing
This path is a deep dive into testing in React. First, you will learn test-driven development in React that will serve as basic for advanced concepts further explained in this path. Then, this path will teach unit testing and integration testing using tools such as Jest and Selenium. Finally, this path will explain end-to-end testing of React apps in detail.
In Jest, matchers are functions that are used to test if a value meets certain criteria. Other than testing the expected flow, Jest also allows testing for error and exceptions. This is an important part of writing high-quality software. Apart from the various matchers available in Jest, there are additional matchers that extend its functionality that are provided by external libraries.
Test Automation
Test Automation allows us to validate and automatically review software products like web applications so we can check their quality standards. Test Automation is rapidly evolving around the globe because of its efficient auto-reviewing and validating process. This path will teach you to design a test automation framework for automating web-based and mobile applications. Moreover, it will help you automate performance testing using Gatling, Puppeteer, and Lighthouse tools. By the end of this path, you’ll be able to design your own test automation frameworks for web and mobile applications.
A test checks if Y
is true or false given X
. The X
can be an argument, API response, state, or user behavior, while Y
can be a return value, error, or presence in the DOM. To test, use expect(value)
and a matcher like .toEqual(expectedValue)
. The evaluation of the value in a test is called an assertion and it’s done using matchers, which are a set of functions accessible via expect
. For example: expect(getSum(1,2)).toEqual(3);
is a way of writing the test in Jest. There are various types of matchers in Jest.
Jest supports several types of tests, but this blog will focus only on the following:
Overall, Jest provides a comprehensive testing solution for JavaScript developers, with support for a wide range of testing types and easy integration with other tools and frameworks.
Although unit tests are the most basic type of test, they play a critical role in ensuring the stability of a system. Unit tests are essential for creating a dependable and resilient testing suite since they are straightforward to write and determine what to test. Additionally, writing unit tests helps to enforce best practices in code.
Unit tests are designed to test individual units of code, such as a single function, conditional statement, or low-level display component. Focusing on one thing at a time ensures that the code behaves as expected, including edge cases and error handling. Unit tests ensure that a small block of code functions well on its own.
Testing React Apps with Jest and React Testing Library
The skills acquired from this course will allow you to write automated tests on your React app using Jest and React Testing Library, which are the de-facto testing tools for React. These tests will allow you to ship new versions of your app with confidence. The tests will also allow you to increase your release frequency, allowing you to get features to your customers quicker. You will learn how to write robust tests that are resilient to breaking when code is improved and refactored, reducing maintenance costs. The course will teach you how to simulate a user interacting with the app. Beyond that, it will also teach you how to robustly test asynchronous parts of the app. Regular quizzes and exercises will reinforce knowledge throughout the course.
The single responsibility principle states that a function should do only one thing, making testing easier. By separating concerns and creating clean interfaces, test maintenance can be limited to the functions directly related to the changed code. This is important for overall code maintenance and scalability, but it also helps with testing, allowing the focus to remain on one section and one test suite when making changes to the code base. Here are three simple example codes for unit tests in Jest.
1. Testing a function that adds two numbers
function addNumbers(a, b) {
return a + b;
}
test('adds two numbers', () => {
expect(addNumbers(2, 3)).toBe(5);
});
In this example,
addNumbers
is defined, which takes two numbers as inputs and returns their sum.expect
method is written to verify that addNumbers(2, 3)
returns 5
.2. Testing a function that converts Fahrenheit to Celsius
function convertFtoC(fahrenheit) {
return (fahrenheit - 32) * 5/9;
}
test('converts Fahrenheit to Celsius', () => {
expect(convertFtoC(32)).toBe(0);
expect(convertFtoC(68)).toBe(20);
expect(convertFtoC(100)).toBeCloseTo(37.7778);
});
In this example,
convertFtoC
is defined, which takes a temperature in Fahrenheit as input and returns the temperature in Celsius.expect
method are written to verify that convertFtoC
returns the correct output for three different inputs.3. Testing a function that filters an array
function filterArray(arr, condition) {
return arr.filter(condition);
}
test('filters an array', () => {
const input = [1, 2, 3, 4, 5];
const expectedOutput = [2, 4];
const isEven = (num) => num % 2 === 0;
expect(filterArray(input, isEven)).toEqual(expectedOutput);
});
In this example,
filterArray
is defined, which takes an array and a condition function as inputs and returns a new array that only contains elements that satisfy the condition.[1, 2, 3, 4, 5]
, an expected output array [2, 4]
, and a condition function that filters for even numbers.expect
method is used to verify that filterArray(input, isEven)
returns expectedOutput
.Code is complex, and testing large sequences of code can be difficult to do well. Unit tests allow developers to concentrate on one thing and test it thoroughly. Once this is done successfully, that block of code can be used elsewhere with confidence because it has been tested thoroughly. Breaking up code into smaller, independent units allows those units to be strung together in more complex ways through a few additional tests.
Writing tests can be challenging and time-consuming, but certain best practices in code can make writing tests more accessible, less tedious, and less time intensive. The DRY principle, which stands for ‘don’t repeat yourself,’ encourages the abstraction of common code into a unique, singular function, making code more maintainable and saving time. When it comes to testing, this principle allows logic to be tested only once, rather than for each context in which it is used.
While unit tests are designed to test individual units of code, integration tests verify that different parts of the system work well together. Integration testing is a critical part of software testing since it can uncover issues that may not be apparent when testing individual units of code.
Integration testing typically involves testing the interaction between multiple components, such as different modules or services within an application. For example, an integration test might ensure that data is correctly passed between a front-end component and a back-end API. Integration tests can also verify that third-party services, such as payment gateways or authentication providers, are correctly integrated with the system.
Complete Guide to Testing React Apps with Jest and Selenium
This course will teach you to properly test your React applications, starting from basic unit tests with Jest to automated integration tests using Selenium. You will learn everything from the ground up to the most advanced concepts that will give you confidence in shipping your software to customers.
Integration tests can help uncover issues such as miscommunication between different components, incorrect data formatting or transfer, and compatibility problems between different parts of the system. These issues can be challenging to uncover with unit tests alone, making integration testing a critical part of a comprehensive testing strategy. Here are three example codes for integration testing in Jest.
1. Testing a simple Express route
const request = require('supertest');
const app = require('../app');
describe('GET /hello', () => {
it('responds with "Hello, world!"', async () => {
const response = await request(app).get('/hello');
expect(response.statusCode).toBe(200);
expect(response.text).toBe('Hello, world!');
});
});
In this example,
supertest
library makes an HTTP request to the Express app and verifies that the response has the expected status code and body text.2. Testing an API endpoint with supertest
const request = require('supertest');
const app = require('../app');
describe('GET /api/books', () => {
it('responds with a list of books', async () => {
const res = await request(app).get('/api/books');
expect(res.statusCode).toBe(200);
expect(res.body).toHaveLength(3);
expect(res.body[0].title).toBe('Book 1');
});
});
In this example,
GET
request is sent to an API endpoint /api/books
implemented in app.js by using the supertest
.200
) is checked, the response body is an array of three books, and the first book has the title Book 1
.3. Testing a database connection with knex
const knex = require('knex');
const config = require('../knexfile');
const { createTable, insertData, getData } = require('../db');
describe('database connection', () => {
let db;
beforeAll(async () => {
db = knex(config);
await createTable(db);
await insertData(db);
});
afterAll(async () => {
await db.destroy();
});
it('retrieves data from the "books" table', async () => {
const books = await getData(db, 'books');
expect(books).toHaveLength(3);
expect(books[0].title).toBe('Book 1');
});
});
In this example,
knex
, which connects to the database.getData
function is checked to return an array of three books, and the first book has the title Book 1
.While integration testing can be more complex than unit testing, there are several best practices that can make it easier to write effective integration tests. One approach is to use mocks or stubs to simulate the behavior of external services or components that are not available during testing. This approach can help isolate the system being tested and reduce the complexity of integration testing.
Another best practice is to use a test environment that closely mirrors the production environment. This can help ensure that the system is tested under realistic conditions and that any issues uncovered during testing are likely to occur in production. Additionally, it is essential to automate integration tests as much as possible to ensure that they run regularly and consistently.
Snapshot tests are another type of test that Jest supports. They capture a snapshot of the output of a component or function and compare it to a previously saved version. This type of test ensures that the output of a component or function remains consistent over time and prevents unexpected changes.
Snapshot tests are useful for UI components because they ensure that changes to the interface are intentional and predictable. When a snapshot test fails, it means that something has changed in the UI that was not expected and that it’s necessary to investigate the change and determine whether it is intentional or not.
Testing Vue.js Components with Jest
Front-end frameworks, in general, are exploding in popularity, and Vue.js, in particular, has become one of the most popular ones. As front-end frameworks continue to grow in usage, the ability to incorporate testing has become an essential feature. It's simply the only scalable way to ensure your app is functioning as expected. You can learn to test your Vue.js applications thoroughly using Jest with this interactive course. You'll start by learning the simplest unit tests in Jest, and will gradually build up to more intricate tests of different Vue components. To give you practical front-end testing experience, you'll be running your tests on a predefined data rendering Vue.js application. This course is essential for anyone looking to create robust and effective front-end tests for their Vue apps.
To write a snapshot test, Jest takes a snapshot of the component or function’s output and saves it as a file. The next time the test runs, Jest compares the current output to the saved snapshot. If the output has changed, Jest highlights the difference and prompts the developer to either update the snapshot or investigate the change. Here are three examples of snapshot tests in Jest.
1. An example code for a button click
import React from 'react';
import renderer from 'react-test-renderer';
import MyButton from './MyButton';
describe('MyButton', () => {
test('onClickHandler renders correctly', () => {
const handleClick = jest.fn();
const tree = renderer.create(<MyButton onClick={handleClick} />).toJSON();
expect(tree).toMatchSnapshot();
tree.props.onClick();
expect(handleClick).toHaveBeenCalledTimes(1);
expect(tree).toMatchSnapshot();
});
});
In this example,
onClickHandler
function of MyButton
component works as expected is checked.handleClick
is first created using jest.fn()
, which will be passed to the component as the onClick
prop.renderer.create()
, and a snapshot of the initial tree is stored using toJSON()
.onClick
event is triggered by calling tree.props.onClick()
, and whether handleClick
was called once using toHaveBeenCalledTimes(1)
is checked.toMatchSnapshot()
is checked.2. An example code that selects an item from a dropdown list
import React from 'react';
import renderer from 'react-test-renderer';
import MyDropdown from './MyDropdown';
describe('MyDropdown', () => {
test('selecting an item from dropdown renders correctly', () => {
const handleChange = jest.fn();
const tree = renderer.create(<MyDropdown onChange={handleChange} />).toJSON();
expect(tree).toMatchSnapshot();
const select = tree.children[0];
select.props.onChange({ target: { value: 'item2' } });
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith('item2');
expect(tree).toMatchSnapshot();
});
});
In this example,
MyDropdown
component works as expected is checked.handleChange
using jest.fn()
is first created, which will be passed to the component as the onChange
prop.renderer.create()
is used to render the component and store a snapshot of the initial tree using toJSON()
.target.value
of item2
.handleChange
was called once with item2
using toHaveBeenCalledTimes(1)
and toHaveBeenCalledWith('item2')
is checked.toMatchSnapshot()
is checked.3. An example code for editing a text field
import React from 'react';
import renderer from 'react-test-renderer';
import MyTextField from './MyTextField';
describe('MyTextField', () => {
test('editing text field renders correctly', () => {
const handleChange = jest.fn();
const tree = renderer.create(<MyTextField onChange={handleChange} />).toJSON();
expect(tree).toMatchSnapshot();
const input = tree.children[0];
input.props.onChange({ target: { value: 'updated text' } });
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith('updated text');
expect(tree).toMatchSnapshot();
});
});
In this example,
MyTextField
component works as expected is checked.handleChange
is created using jest.fn()
, which will be passed to the component as the onChange
prop.renderer.create()
, and a snapshot of the initial tree is stored using toJSON()
.target.value
of updated text
.handleChange
was called once with updated text
using toHaveBeenCalledTimes(1)
and toHaveBeenCalledWith('updated text')
is checked.toMatchSnapshot()
is checked.Snapshot tests are helpful when designed and maintained properly. However, Jest cannot distinguish between intentional and unintentional changes in our user interface. Client-side code can be placed into two buckets—direct UI changes and functionality changes. Direct UI changes require updating snapshots, while functionality changes may or may not require updates. If a bug in the functionality affects what the user sees, snapshots may need updating. If changes fundamentally impact the end result for the user, snapshots will need updating.
Snapshot tests can also be helpful when refactoring code or when making small changes to an application. They provide a way to ensure that changes don’t inadvertently affect other parts of the system. While snapshot tests are not a replacement for unit tests, they complement them and help to create a robust and dependable testing suite.
Jest provides a powerful set of tools for developers to create and maintain unit, integration, and snapshot tests. These tests enable developers to identify issues early in the development process, ensure that changes to one part of the system do not break the other parts, and monitor changes to the user interface. By leveraging Jest for testing, software development teams can improve the overall quality of their code, increase efficiency, and reduce the risk of introducing new bugs and issues.
Educative currently offers the following career-specific paths for developers:
The following specific courses on software testing are also offered by Educative:
Happy learning!
Free Resources