So what is testing after all? Don’t we test all the time when we just click around in our app?

Well, this is testing, you are right. But it’s manual testing. We test by clicking around.

To some extent, this is a good thing to do. Actually, you will always be doing that because you want to experience your app on your own.

But writing automated tests - and that is what this article is mainly about - speeds up your development flow and gives you a way of quickly identifying issues, breaking changes and side effects.
If you got a bunch of automated tests, you can quickly spot problems when you start working on your code and suddenly one or more of your tests fail.
Automated tests, by the way, are really just code snippets that run your code and then check if the result of that code execution meets a certain expectation. But you’ll see all that below.

The answer is trivial: It speeds up development because you don’t have to test everything manually after every change.

Additionally, it’s less error-prone. When testing code manually, it’s easy to overlook a certain scenario and therefore to overlook a bug.

Of course you can also write bad automated tests, you can forget an important scenario there, too. But over the lifespan of your project development, you’ll very likely encounter issues and add respective tests.

Additionally, if you write tests, you’re forced to think about your app and potential issues harder. You have to come up with clever tests that will fail if something important changes.

You are also forced to write cleaner, more modular code because the more spaghetti your code becomes, the harder it will be to test.

When talking about “tests” or “automated tests” (I’ll use these terms interchangeably), we can differentiate between three kind of tests:

Unit tests that test one isolated unit/ piece of code (e.g. a function)

Integration tests which test the combination of features (e.g. a function calling another function)

End-to-End (e2e) or UI tests which test a full interaction path in your app (e.g. the signup process)

These kind of tests have a different level of complexity to write them and a different frequency with which you’ll write them.
# Unit tests
Unit tests are the easiest tests to write because you have some input and can expect some result. There are no dependencies, no complex interactions.

Here’s an example:

Consider this function which we use in our app - it’ll take name and age as an input and return some text that contains these two parameters.

constgenerateText=(name, age)=>{return`${name} (${age} years old)`}

Here’s a fitting unit test:

test('should output name and age',()=>{const text =generateText('Max',29)expect(text).toBe('Max (29 years old)')})

This test will check whether the generateText function does return the expected text.

If we now change the generateText function, let’s say like this:

constgenerateText=(name, age)=>{return`${age} (${age} years old)`}

Then our test will fail. Because this function would return '29 (29 years old)' instead of 'Max (29 years old)'.

I’ll come back to where the test and expect functions are coming from!

For that reason, you should split your app into a lot of small modules which you can test individually. This will lead to cleaner code as a nice side-effect.

For the same reason, you’ll also write a lot of unit tests in a project. It’ll be your most common form of tests. If you test all the individual units of your app, chances are high that the app as a whole will also work.

Integration tests are a bit more complex than unit tests because you now have to deal with some dependencies (e.g. another function that gets called). These dependencies of course also have an impact on the result of your test, hence it’s important to write “good” tests which allow you to understand what kind of effect leads to which result.

It’s also important to unit-test the dependencies of your integration test as this will help you narrow down issues.

You could also think that integration tests are redundant if you got unit tests for everything.

But that’s not the case. Here’s an example (which you also see in the above video):

There’s no special syntax as you can see. It’s a normal test. We just call it integration test because it tests something which does have dependencies.

The checkAndGenerate function returns the result of generateText in the end but before it does so, it also validates the input. It does all that by calling other functions - hence we got a dependency here.

On first look, you could think that this will only fail when either validateInput or generateText have a problem - which would of course be issues that should be detected by a unit test. So why should we test the checkAndGenerate function?

Well, here’s the answer. Consider this change to the checkAndGenerate function:

What changed? I removed the ! in front of the first validateInput call.

That will now break the logic of this function since we now handle the result of validateInput incorrectly. So neither validateInput nor generateText are broken and still checkAndGenerate would yield an invalid result.

And since it involves this, we need a browser. Actually all tests run in the browser but they’ll not load up your app. They just need a browser JavaScript environment (i.e. essentially an empty browser window that’s loaded up behind the scenes).

For end-to-end/ UI testing, we need a browser that loads our app though. And we need to be able to control that browser via code (so that we can program certain user interactions and simulate them).

We only write it to execute it during development, it will not be shipped together with your app code. It will never run in the browser of your application users. That’s really important to understand!

Instead, we need some tools that allow us to execute our tests locally, define our expectations (and check them) and control the browser for e2e testing.

In short, we need three kinds of tools:

A test runner that executes your tests (test()) and summarizes the results

You also need to be able to define your expectations and check them. Assertion libraries like Chai help you with that.

But here, we also can use Jest! And that’s the cool thing about it. Besides being powerful and all that, it’s also not just a test runner but test runner + assertion library combined. Another reason for its popularity.

Puppeteer is a headless version of the Google Chrome browser. And it’s even developed by the Google Chrome team. It’s meant to be used as a headless version of Chrome (though you can even run it with an UI attached) and it’s great for automated testing.

Puppeteer - which I use in this article + video - can be installed with this command:

With the “WHAT”, “WHY”, tooling and kinds of tests explained - it’s time to write tests, right?

You saw snippets above and you can see the full project in the video that you find at the top of the page.

Tests are really all about defining code that should be executed by the test runner and checked via the assertion library.

With Jest, you can define a new test with the test function. It’s globally available when running Jest. Jest will automatically execute files that end with .spec.js or .test.js hence you should place your tests in there.

We launch a browser (that can be controlled via the test) with puppeteer.launch. This browser object can then be used to create new pages (newPage()), navigate to different URLs (goto()) and interact with the page (e.g. click()).

Since all that code interacts with a real browser, we can then also use some built-in methods (e.g. $eval) to evaluate DOM elements. In the example, we extract the textContent of a created element. At the end, expect is used again to check whether the created element has a valid text or not.