How to Test Your Web Apps
In the software industry, everyone tests their applications in some way. Many developer teams have elaborate testing schemes that are well-integrated with their continuous integration pipelines, but even those who don’t have automatic tests still have to come up with ways to check that their code performs as intended.
Building a website and clicking through it manually with the help of a browser is a variety of testing. While it is not the most sophisticated one, it still counts. The same is true for firing up cURL in the console and sending a synthetic request to the API endpoint you just created.
This post will discuss how to automate the manual testing we already do before diving into different test types and methodologies.
Automating Tests
Manual tests are sufficient for small apps, but, when these apps grow, their testable surface grows with them, causing two problems.
One, when people have to perform tests every time a new version of an app is finished, inconsistencies can arise. This is especially true if there are many tests. Two, the person doing the tests cannot perform other tasks. With big apps, testing might take multiple days.
The most logical way to solve these two issues is to automate these manual tasks. Two main types of tests exist for web apps: UI tests and API tests. Two tools can automate them.
UI Tests
UI tests can be performed with a tool called Puppeteer. Puppeteer allows you to automate a manual UI test using JavaScript. It is installed via NPM with the following command:
$ npm i puppeteer
You can then write a script that controls a headless version of Chrome to run your tests. This script could look like the following:
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com', {waitUntil: 'networkidle2'});
const heading = await page.$('h1');
await browser.close();
if(!heading) console.log('No heading found!');
})();
In this script, we are starting a headless instance of Chrome, navigating to example.com, and looking for an h1 element. If it doesn’t exist, an error message is logged.
API Tests
API tests can be automated via Postman. Postman comes with a graphical interface for building HTTP requests that can be saved. These requests can be executed later with just a mouse click.
To get started, download and install the Postman UI. The following steps are needed to create and save a request:
- Click Create a Collection on the left
- Enter My Collection as the collection name
- Click Create
- Click on the ellipsis (…) of My Collection on the left
- Select Add Request
- Enter My Request as the request name
- Click Save to My Collection
The request shows up in the left sidebar under your newly-created collection.
If you select it, you will have to enter example.com as a URL and add a test. To add a test, click on Tests in the tab bar below the URL input field.
A text area in which you can write your test in JavaScript will appear. The following is an example of a test:
pm.test("Status code is 200", () => {
pm.response.to.have.status(200);
});
If you click Save at the top right corner of the screen and Send right after it, your request will be sent. The test script will then execute to determine whether or not the response had status 200.
Types of Tests
There are three different types of tests. Understanding them enables you to choose the right type of testing for the various parts of your software stack.
Unit Tests
Unit tests are the simplest type of test. They check the correctness of small parts of your code. A unit test usually covers one call to a function or one way to use a class.
A unit test is usually the first “user” of your code. Some test methodologies even require you to write the test before the implementation of the actual application code.
Unit tests can be used in both backend and frontend development. Some development teams use unit tests to check the rendered DOM output of small parts of their code, like forms or menus. In the backend, unit tests are often used to test the code between the HTTP server and the database or other APIs.
Three widely used test runners for unit tests are:
‐ Jest—Jest is mostly used in frontend development because it comes with unique features, like snapshot testing, which help with problems like UI regressions. A Jest tutorial can be found here.
‐ AVA—AVA is favored in backend development because it specializes in highly parallel execution of tests. An AVA tutorial can be found here.
‐ PHPUnit—PHPUnit is a popular framework for unit tests in PHP. A PHPUnit tutorial can be found here.
If a unit test requires some external resources, like network connections or databases, then you are probably writing an integration test. This type of test is covered next.
Integration Tests
From an implementation perspective, integration tests look like unit tests. The difference is that integration tests test multiple parts of the stack together. For example, they might test whether or not the client and server speak the same protocol or, in a microservices architecture, whether or not the services are working correctly together.
Checks that usually would end up in multiple independent unit tests can be aggregated into one integration test that determines if everything is working well together.
Integration tests are used in both frontend and backend development. They are sometimes used to see if the two parts are interacting correctly, but they can also be used to determine if different modules of one part are working together as intended.
You can use the same test runners for integration tests that you used for unit tests. However, Postman, the UI tool used above to automate manual API tests, also comes with a CLI tool called Newman that can be integrated with your CI/CD pipeline.
Newman can run exported Postman collections, allowing you to create requests and tests with the Postman UI and run them later via CLI. A Newman tutorial can be found here.
If an integration test requires interaction with the UI, it’s called a UI test—addressed next.
UI Tests
UI tests are the most sophisticated tests. They try to emulate user behavior in an automated fashion so that testers don’t have to click through every part of their app manually.
UI tests often help to capture a specific interaction that led to an error for a user. Once it is captured, it can be reproduced with one click in order to fix the bug and prevent it from coming back in a new version.
The Jest and AVA test runners mentioned before can be used here, but you will usually need an extra library to facilitate the UI interaction via a browser. The two main libraries currently in use for this process are:
‐ Puppeteer—Puppeteer is a JavaScript library that comes with a headless implementation of Chrome that allows running UI tests programmatically in the background. A Puppeteer tutorial can be found here.
‐ Selenium—Selenium is a framework that allows the remote control of a browser via a library called WebDriver. A Selenium tutorial can be found here.
There are more types of tests than the ones listed here. Others may have different goals; for example, load tests try to find performance bottlenecks. Keep in mind that the tests described here are sometimes given different names. The three types presented above are the essential ones to implement when getting started. Using one of them is better than using no automated testing at all.
Test Methodologies
Test methodologies are ways to think about testing. The three described below are the most commonly used ones.
Test Driven Development (TDD)
TDD is the most widely used methodology and the most technical one. It recommends that you write your tests before you write the actual code that you want to test. Since you have to write a test for only the part of the code you are implementing, the type of test you will write is a unit test.
Unit tests are rather granular, so, over time, use of this methodology results in the accumulation of many tests. This reality, paired with the fact that beginner TDD practitioners tend to write trivial tests, can lead to the creation of a pile of useless tests. To avoid this, it’s crucial to update and clean up tests when the team has gotten more familiar with TDD and with testing in general.
It’s important to run the tests frequently, not only in a CI/CD pipeline, but also locally on your development machine. Write a test, run it, see it fail, implement enough to make the test pass, and repeat the process.
For further reading, check out Agile Alliance’s description of TDD.
Acceptance Test Driven Development (ATDD)
ATDD is a modification of TDD that focuses more on business cases and less on technical implementation. The test types that are used in this methodology are mostly integration tests, because, often, multiple parts of the system need to be used in conjunction to solve a business need.
Since the tests are less technical, it’s also advisable to include non-technically-oriented people in the testing process. Product owners and customers can help to define the business cases so that a developer can write a test for them. If the test can’t be run successfully, it’s clear that more functionality has to be implemented.
ATDD works on a different abstraction level than TDD, so the two methodologies can be used together.
For further reading, check out Agile Alliance’s description of TDD.
Behavior Driven Development (BDD)
BDD is a mix of TDD and ATDD, and its goal is to use the best of both worlds to make the overall system more stable. BDD tries to specify a system with tests that illustrate its usage.
Like ATDD, BDD tries to capture business cases. However, it also requires you to question these use cases with the “5 whys” because the “whys” are a missing part in ATDD. Sure, it’s better to ask customers what they want instead of relying solely on developer input. But, it’s also important to question the assumptions of both of those groups.
BDD is also a mix of TDD and ATDD in the sense of abstraction levels. TDD only tests the small parts of your application and ATDD only tests how these parts work together. BDD requires you to apply this methodology to the big whole of your app and its small parts, making BDD a more holistic approach to testing.
For further reading, check out Agile Alliance’s description of BDD.
Conclusion
While no testing is terrible, manual testing is better, and automated testing is the best.
Depending on the size of your app, a manual test can block members of the development team from working on other projects for days or even weeks, costing your business time and money. In addition, the monotonous task of performing the same interactions over and over again can lead to slips that often manifest in uncaught bugs.
Computers are very good at repeating the same task over and over again, so it’s a good idea to delegate testing to a script and free up your developers’ time. If these tests are integrated into your CI/CD pipeline, the already-written tests can just run implicitly when a new commit lands in your repositories.
It’s a good idea to employ a testing methodology early on, because, frequently, the big problems when getting started are “what” and “when” to test. These methodologies can help to clarify a process that makes writing tests more consistent for all members of the team.