What is software testing?
Software testing is an activity that is defined by feeding some data into a piece of software and expecting a certain output. It sounds simple but can be very hard to select a good set of test inputs and design good acceptability tests.
Testing is crucial in making sure that the application behaves as expected. Software bugs can potentially cause monetary or human loss. There are lots of examples in history that led to catastrophic failures.
In my professional career, I have witnessed codebases that do not have any tests at all, or codebases that have really weak test coverage. Test coverage is a metric that measures the amount of the application code that is covered by tests. It takes time to write good tests. Of course, it is quicker to implement features, do a couple of quick manual checks and release code to production. However, in the long run, properly written tests could save hours of manual debugging and unexpected application crashes.
If your company does not encourage developers to write tests (or perhaps, does not understand the value), it is your duty as an engineer to explain the benefits to the business owners.
Catch bugs early
If you do not follow Test Driven Development (which is a whole different topic), adding tests is your chance to catch bugs early on and prevent the world from seeing them. If you add new code/features to the application, you can check how the system reacts to them.
Code quality (refactoring or changing implementation)
We always look for ways to improve our code. We spend time refactoring and making sure that it adds value to the business. Sometimes we figure out a better solution and change the implementation. Now, imagine that you refactored part of the codebase and everything seemed to work fine. You pushed to production and baaaaam after a couple of hours the application crashes. Don’t be that person. You could potentially waste an enormous amount of time trying to find the root cause. But if you had tests in place you probably would have found out that some part of your application does not respond well to the refactor. You could have just run the test suite and identified what needs to be fixed. This is not always the case but the majority of cases are straightforward. I am also sure that your customers would have appreciated if you took longer to deliver the service that was stable and reliable.
In this post, I would like us to go through the common types of application testing that developers perform. Below is the visual representation of the key concept in automated testing: the test pyramid model. It shows three different layers of testing and the proportion of tests that a project should have (there should be more unit tests than integration tests and more integration tests than end-to-end tests).
We will cover all three types. Unit and integration testing is categorised as functional (testing the functionality of the system, making sure that functions perform as expected). End-to-end testing is categorised as non-functional (testing system as a whole). There are other categories and many different types but these are the most common ones. This image serves as a good rule of thumb: an application should have different levels of testing and the higher you go in the test pyramid the least tests you write.
Before diving into various testing methods and tools, let’s look at some of the terminology used.
Assertion – this is when we test for a specific condition and expect a certain outcome. For example, we store in a variable a result of some calculation and assert that the variable is equal to an expected value.
Spy – records interaction with the outside world. We can test spies and get a number of calls to functions, return values and exceptions.
Stub – holds data and answers to the calls made during the test. It allows us to not use objects that provide real data. For example, we could stub a method on a model and return the desired value.
Mock – a fake object that mocks the behaviour of the larger system in which the module under test lives. We have the full control over the mock so can easily be configured. We could mock a dependency for example.
A unit test is where a piece of code is tested in isolation. It could be a UI component (a form, a slider, etc.), a module or a class. All of the dependencies are faked (meaning that they are replaced with fake, controlled representations) as we want to isolate and only be concerned with a unit of code. We should not worry about the internals of the module but with the results it produces. If there are many exposed methods in a module, you should not put all of the assertions into one unit test. A unit test should test only one concern.
An integration test is where multiple units of code/modules are tested together. Take banking as an example. An integration test could be checking an interaction between a payment module and a balance module. If you are familiar with React framework, a test could be checking an interaction between a React container that processes user details and a user model that contains methods available on a user object.
E2E (end-to-end) tests
An end-to-end test is where an application is tested as a whole. It checks the flow from start to finish. These tests simulate real-world scenarios under real-world conditions (communication with various parts of the app, using a network, interacting with hardware etc.). When performing these tests we essentially simulate user actions. Because these tests involve checking many different parts of the application, they are slow. Referring back to the test pyramid, we can see that there should not be many end-to-end tests. We should only be testing the main flows of the application. End to end tests are there to test the glue layer between unit and integration tests.
Below is an example of an end to end test flow:
A user lands on a homepage of an application
A user clicks on a login button
A user is sent to the login page
A user enters a valid username and password
A user is then redirected to the members-only dashboard
A user clicks on a logout button
A user is redirected back to the homepage and a successful “See you soon again” is displayed
JS testing tools
- Jest (developed by Facebook, great for testing React applications)
- Mocha (developed and maintained by the community, can be used both on Node.js and in the browser)
- Ava (minimalistic and fast testing framework with simple syntax)
- Jasmine (sutable for running both Node.js and browser tests)
For assertion testing, we need to pick an assertion library. Node.js has a built-in assert module that provides a simple set of assertion tests. There are a number of assertion frameworks with simple APIs.
- should.js (expressive and clean, with helpful error messages)
Spies, stubs and mocks
- Sinon.JS (test framework agnostic JS test spies, stubs and mocks)
- Cypress (test anything that runs in the browser)
- Nightwatch (can be used for end-to-end as well as unit and integration tests)
- CodeceptJS (clean and intuitive API, based on Mocha testing framework)
- Istanbul (track your unit test coverage)
Software testing is a very interesting, deep and complex subject. We have only scratched the surface in this post. I’ll provide some resource if you are interested in learning more.
Remember that quality cannot be tested into bad software. It all starts with clean, structured and decoupled code.