My hope is once you read this article you understand what testing is, the types of tests we currently use, and their benefits.
Testing could feel painful at the beginning and one of my goals is for you to lose it and enjoy it.
This article has been written for beginners but also for people who want clarification, for that reason, it’s intended to be tool independent.
Content
- What Is A Test?
- Testing ≠ Debugging
- Early Reasons For Testing
- The Manual Test Way
- Advantages of Automatic Tests
- Anatomy of A Test
- Types of Tests
- Test-Driven Development (TDD)
- Good Practices
What is a test?
To know what we are doing we first need to define what is a test.
Writing tests is writing a requirement list for the things you want to build, for that reason a test is a specific requirement we must fulfill.
Testing ≠ Debugging
Catch ≠ Kill
Sometimes when we start testing, a common mistake would be to confuse debugging with testing and don’t know where one ends and the other begins.
“Testing is weaving a spider web of tests to catch bugs, when a bug is detected we get notified and then it’s time to debug it.”
Testing
It can be seen as writing a list of requirements (remember the second image) that our feature must meet, which means, we could write our tests before writing a single line of code if we want. Can be done manually or automatically.
Debugging
It is fixing an error, generally speaking, there is no error if there is no code, so it requires getting dirty with it, and only makes sense to do it manually.
Early Reasons For Testing
From the things we know until now we can say:
- We will work towards objectives and not assumptions or in features that don’t add value, increasing the quality of our code.
- Helps us with the early detection of errors, avoiding spending time on things that will ultimately be a disaster.
- Acceptance criteria could be clearly defined through our tests, avoiding redundancies and misunderstandings among stakeholders.
The Manual Test Way
Now that we know what testing is and what is not it’s time to make a distinction between manual and automatic testing and see its differences.
When we modify the code by adding, deleting, or updating a feature inside of it we want to be sure we don’t introduce errors. Let’s take a look at how we handle it using manual tests and some problems that emerge:
Case 1
Let’s say we add a new feature, feature D, and this one is broken… as we go ahead it’s likely that we can catch the error, no problems here.
Case 2
Let’s say we add feature D, and unintentionally break feature E in the same component. In this case, it is highly likely that we might not catch this error. But suppose that we are focusing on just one component and we see that something is breaking it. A little harder… but still possible to catch the error.
Case 3
Let’s say we add feature D and unintentionally break feature G which is in another component; in this case, is very unlikely that we don’t catch the error. We say that’s fine, push the changes and then, in production, we discover that feature G is failing.
With manual testing is almost impossible to check every single component and see if it is working properly, wouldn’t it be better to write tests for each feature, and once a change has been made run all or part of them automatically and then get notified if something is wrong?
Advantages of Automatic Tests
Why should we use automatic over manual tests? A few good reasons:
Saves time
Yup 😜, you read it well, we write a test once and run it each time a change is made, automatically. Manual testing implies making a test each time you visit a feature, over and over again, it’s time-consuming and we might forget to evaluate something on an iteration.
Manual test is error-prone
We might not be aware always when something fails as we saw in our previous section.
Serves as documentation
The tests we left serve as documentation (remember, each one is a requirement). If another developer wants to understand your code he/she probably prefers to read your tests to understand the code specifications.
Prevents dirty features
We can isolate tests in other files without carrying about ‘cleaning’, avoiding unnecessary checks… like console.log inside the code.
Makes debugging easier
If we use atomic tests… when a specific test fails we can know exactly what went wrong.
Anatomy of A Test
No matter what script, library, framework, or even the type of test we use it always involves the same:
“Set a domain, run it, and check if the range meets our expectations.”
Why Mocks
When we run our tests we use objects that mimic the behavior of real objects in a controlled way (mocks) so we can establish a cause-effect relationship between our inputs and outputs, isolating our features from external forces that might produce unpredictable behaviors.
For example, if our feature would make real HTTP requests the tests could be slow or unpredictable (if the server goes down or if the response changes over time) or even costly (we may not want to run our server every time a test runs).
Types of Tests
As we might expect, each part of our application leads to different types of tests. The difference between them is like the domain, range, and method of comparison:
Implementation (or Unit) testing
Brings a test over a fundamental unit of code, usually functions or classes. It’s highly attached to how the code was written.
Test algorithm
- Get a function or method.
- Insert inputs and call it.
- Compare the response.
Use case
- Review code quality.
Functional or Behavioral Testing
Tests how users use our software and not our internal implementation as unit tests. Includes all relevant components that involve certain user behavior.
Test algorithm
- Builds a virtual representation of a UI element.
- Simulate a user interaction over the element: clicks, scroll, mouse over, etc.
- Compares the behavior with the expected result.
Use case
- Meet user stories, 1 user story ~ 1 functional testing.
Visual Testing
Evaluate the UI according to the expected design, works over the user interface looking at what happens when clicking a button or scrolling a page, etc.
Test algorithm
- Builds a virtual representation of a UI element.
- Simulate a user interaction over the element: clicks, scroll, mouse over, etc
- Compare the response visually with the original design.
Use case
- Sync UI developers and designers.
Integration (or API) Testing
Focused on checking data communication between modules of code, and API communication.
Test algorithm
- Get an API.
- Call it.
- Check structure and content.
Use Case
- Validates the data structure and its content from our APIs.
Acceptance Testing
It's performed by users after testing the system and before making it available for actual use on market.
Test algorithm
- People outside the developer team use the app in production.
- Gather real metrics.
- Compare it with business metrics.
Use case
- Validates if business requirements are met.
Types of acceptance tests
- Internal Acceptance (or Alpha) Testing is performed by members of the organization that developed the software but who are not directly involved in the project.
- Customer Acceptance Testing: performed by the customer of the organization that developed the software.
- User Acceptance (or Beta) Testing is performed by the end-users of the software.
Test-Driven Development (TDD)
As we saw, testing doesn’t require code implementation to be done, TDD is the practice of writing our tests at first and later writing the code.
Steps:
- Write “shell” component or feature.
- Write the tests and import your shell component into them.
- Fail the tests.
- Write code until it passes the tests.
Benefits:
Testing neglect is avoided and requirements are enforced by ensuring that your code conforms to testing and not vice versa, and also helps to write better code from the beginning making us think in requirements before we write a single line of code.
Good Practices
So far, doing automatic testing sounds like the best solution, and it is, but a bad implementation can turn it into a nightmare. I trust that by having clear concepts, following the next suggestions, and practicing a little (with the tool you want), you will avoid the most common mistakes.
For testing
- Run tests in isolation between other tests and other components. One file per component, one or more tests per feature, and one test for one requirement.
- Test edge cases and error paths not just happy ones, because we want our tests to cover a wide range of scenarios.
- Establish a domain and range (inputs & outputs) for each feature we are testing, in this way, when a certain input is received we can be sure of the type of result that we can expect.
For Coding
Apply SOLID principles to write, clean, modular, and descriptive software, avoiding:
- Swamp of global variables.
- Pointer soup.
- Side effects (as possible).