Manual testing is simply navigating through your product and seeing if it will behave as expected or not. To some extent, this is a good thing to do. It’s always helpful to use your own application.
NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.
However, automated tests speed up your development flow and give you a quick way to identify issues, break changes and side effects.
Why Automate Tests?
- Get an error if the code is broken (ex: Tests that automatically trigger a new releaseAt on git)
- Save time
- Think about possible issues and bugs
- Integrate into the build workflow
- Break complex dependencies
- Improve your code
We’ll explore all the above points in detail in our upcoming posts.
Different Types of Tests
Unit Tests: Unit tests are the easiest tests to write because you can expect specific results for your input. There are no dependencies or complex interactions.
Integration Tests: Integration tests are more complex than unit tests because you have to deal with dependencies.
End-To-End: End-to-end tests simulate a specific user interaction flow with your app. For example, clicking or entering text.
From the above figure, we can conclude:
- Unit tests are the least complex and E2E tests are the most complicated.
- We tend to write more tests with less complexity. It’s preferable to write more unit tests than E2E tests.
- The integration tests fit in the middle. This implies that we write them more frequently than the E2E tests but less than unit tests.
Now let’s dive deep into each of the three tests.
Unit Tests
Consider this function which we use in our app. It will take name and age as inputs and return a greeting with these two parameters.
const greeting = (firstName, lastName) => { return `Hello Mr./Mrs. ${firstName} ${lastName}` }
A valuable unit test could be:
test('should output firstName and lastName', () => { const text = greeting('Moataz', 'Mahmoud') expect(text).toBe('Hello Mr./Mrs. Moataz Mahmoud') })
This test will check whether the greeting function returns the expected text. If we now change the greeting function, let’s say like this:
const greeting = (firstName, lastName) => { return `Hello Mr./Mrs. ${firstName} ${firstName}` }
Then our test will fail since it will return Hello Mr./Mrs. Moataz Moataz instead of Hello Mr./Mrs. Moataz Mahmoud.
expect and toBe are predefined keywords in chai assertion library and will be addressed in detail in future posts. So don’t worry about them for now.
From a software engineering perspective, it’s better to put your app into as small units as possible so that you can write unit tests for them. And that’s why we said at the beginning of this article that unit tests are the most frequent tests in a project.
Integration Tests
Integration tests possess the next level of complexity in the testing pyramid. They are more complex than the unit tests because you need to handle code block dependency. You are testing how a code snippet (the method most of the time) depends on another method to run and pass some value to it.
Because of this high dependency between the tests, we highly recommend covering all the single components in the unit testing’s scope.
Since you are covering every single unit independently, you may think that your system is working perfectly. But that’s seldom the case. Two components working independently doesn’t mean that they will work together as expected.
Here is an example:
exports.checkBeforeGreeting = (firstName, lastName) => { if (validateInput(firstName, false) || validateInput(lastName, false)) { return false } return greetThem(firstName, lastName) }
An integration test can be like this:
test('should generate a valid text output', () => { const text = checkBeforeGreeting('Moataz', 'Mahmoud') expect(text).toBe('Hello Mr./Mrs. Moataz Mahmoud') })
You can see that there is no special syntax to do it. It’s the same syntax used in unit tests. It’s just called integration since it’s testing code that depends on another. In this case, it’s greetThem depending on validateInput. And checkBeforeGreeting is called a wrapper method.
At first look, you may think that this will only fail when either validateInput or greetThem has a problem – which would be detected by a unit test. So, why should we test the checkBeforeGreeting function?
Here is the answer: Suppose that the condition in the checkBeforeGreeting got misimplemented:
if (validateInput(firstName, false) && validateInput(lastName, false))
That will now break the logic of this function as we now incorrectly handle the result of validateInput. So even though validateInput or greetThem are not broken, checkBeforeGreeting would still yield an invalid result.
That’s why you need integration tests!
End to End Tests
End-to-end tests simulate a specific user interaction flow with your app, including UI or API tests.
The UI tests (clicking, entering text, etc.) run in the browser, but they’ll not load your app. They just need a JavaScript environment (i.e., an empty browser window that’s loaded behind the scenes).
However, for end-to-end/ UI testing, we need a browser that loads our app. And we need to be able to control that browser via code (so that we can program certain user interactions and simulate them).
The syntax of an end-to-end test depends highly on the used framework. Here is a kind of pseudocode for one:
test('should create an element with text and correct class', () => { const page = new Page() page.visit(baseURL) page.click('input#firstName') page.type('input#firstName', 'Moataz') page.click('input#lastName') page.type('input#lastName', 'Mahmoud') page.click('#btnGreetUser') expect('.greeting-user').toBe('Hello Mr./Mrs. Moataz Mahmoud') }, 10000)
It’s all about telling the browser what to do -by traversing the DOM- and expecting that the exit criteria of the test scenario are succeeding.
API tests don’t need a browser. For example, an API end-to-end scenario can be simply like this:
test('should create a user and then greet him/her', async (Moataz, Mahmoud) => { const user = request('/createUser', 'POST', 'Authentication', { firstName, lastName }) const greetingMessage = request('/greetThem', 'POST', user) expect(greetingMessage).toBe('Hello Mr./Mrs. Moataz Mahmoud') }, 10000)
You can see that these tests are not too challenging (neither API nor UI). However, they can be a little tricky to debug if broken.
There is one tool to do both the UI and API tests — Cypress. We’ll be sharing in-depth Cypress tutorials in the coming weeks. Subscribe to our blog if you’d like to get notified.
This post was published under the Quality Assurance Community of Experts. Communities of Experts are specialized groups at Modus that consolidate knowledge, document standards, reduce delivery times for clients, and open up growth opportunities for team members. Learn more about the Modus Community of Experts here.
Moataz Mahmoud
Related Posts
-
Here's Why You Should Write Unit Tests
Software engineers have been testing ever since they could write code. However, the ability to…
-
Unit Testing w/ AngularJS
Oh how far we've come in the web development world. Cross-browser support and performance, once…