DEV Community

Cover image for Introduction to testing
Taha Shashtari
Taha Shashtari

Posted on • Originally published at tahazsh.com

Introduction to testing

All programmers do testing the very first time they write code in their lives. After you add or modify some line of code, you head to the browser to see the changes. Sometimes, you want to check the new feature you added, and other times you just want to see if something broke after you modified something in your code.

That kind of testing is called manual testing.

If something is called manual, it means there should be an automated version. And there is, and it's called automated testing.

With automated tests, you don't need to go manually testing your changes in the browser most of the time; there should be a command you run to do all the testing for you.

It's easier said than done. That command is something you need to implement; and here comes the other aspect of learning programming, which is learning to write tests.

Functional vs. Non-functional tests

In this article I'm talking about functional tests. Functional tests are the tests that ensure that your code is working as expected. It's testing the behavior of your software—does it do what it should do?

Non-functional tests, on the other hand, are for testing the other aspects of the software, such as testing performance and security.

Great benefits of writing automated tests

The two biggest benefits of writing automated tests are: detecting bugs and refactoring.

Detecting bugs is basically checking if you broke the code after certain changes—the same reason you do manual testing.

Refactoring is also checking if you broke the code but after improving the code. There are many times where you think about improving the design of the code, but you are reluctant to do that because you might break something. With good tests, you don't have to worry about this because you will know if you broke something while refactoring it.

So a common thing developers with tests would do is writing the code the fastest way possible just to make the tests pass, and then refactoring it to make it look better.

Types of tests

If you had ever read anything about testing, it's likely that you have come across these terms: unit testing, integration testing, end-to-end testing, acceptance testing, and the list goes on and on.

Interestingly, each of these types tends to have a different definition. Explaining each one in detail will require a whole article for each one. But in my experience, understanding the following three types of tests the way I (and many others) look at them will save you a lot of time and confusion in the future.

Unit tests

Unit tests are the most basic type of tests. They form the majority of the tests in your code. Some see units as the low-level stuff in your code, like objects and functions. Others, including me, see each unit as a single test that tests some specific behavior in the software. That test might use multiple objects and functions to achieve some goal.

An example would be a test that tests the parsing of CSV string. That test might use multiple objects and functions to achieve that, but it's still a single test that tests a behavior of my software.

Integration tests

Integration tests test the integration between two standalone parts of the system. Some look at integration tests as a way to test the integration of multiple objects and functions in the same system. Others, including me, look at it as the integration between the software's code and other services.

An example would be testing the integration between your app and a third-party payment service. In this case, your app works alone, and the payment service works alone, but does the integration between them work as expected in the context of your app?

End-to-End tests

If unit tests are for testing the internals of your software, and integration tests are for testing the integration with other parties, how can you test if your whole app works as a whole?

That's what end-to-end tests are for. They are the slowest kind of tests. They should run the same way your user would use the app: go to a specific url with some input, and then see something on the page (or get a specific response from that endpoint).

There are tools for that kind of testing. These tools can literally open a new instance of the browser, go to a specific url, and inspect the results on the screen—you can literally see that process happening in front of you, unless you are using headless browser testing.

The speed of tests

The speed of your tests are based on the type of tests you are running and how they are implemented. They can be as fast as a few hundred milliseconds or as slow as several hours.

Unit tests are usually the fastest because they don't need to wait for a response from some external server or run the app in the browser.

Integration tests can be as fast as unit tests or can be slower. This depends on how you implement them: if you are mocking the third-party service (I'll talk about mocks in a little bit), then they will be fast. If you don't mock them, then the speed depends on the performance of that third-party interaction.

End-to-end tests are the slowest because they need to run as if the app is in production. You need to wait for requests and third-party services to complete before seeing the results of your tests.

A good practice is to split your test suites in types so you can run each one separately. A general rule of thumb is to run unit tests on every change because they are the fastest; run integration tests multiple times in an hour; run end-to-end tests a couple times per day to make sure that the whole system works as expected.

Mocking

Mocking is a very a big topic if I want to get into its details. But in its essence, mocking is replacing an element in your code (like object or function) with a fake version.

There are multiple reasons why you would need to do that. One reason is performance. If your tests need to talk to an external service, you can replace that external service with a mock so you don't have to make an actual request, and instead use the mock to get some response.

Some people use mocking for isolating the thing they are testing from the rest of the app—this is called the Mockist testing style. Some people don't like this approach, but it's a valid one used by many great developers.

As I mentioned above, getting into the details of mocking is beyond the scope of this article. So all you need to know is that mocking is a fake version of some element in your tests—most testing tools support creating them.

Test-driven development

You probably heard the term TDD (Test-driven development). TDD is just one way to write and run your tests. From its name, it's a way to derive your code through tests, which means you write tests first and then the code to make it pass.

It might feel awkward at first, but it's actually a good thing to do for two main reasons. Writing the test first ensures you have a test for each code you implement—if you don't do this, you might forget to write the test after writing the actual code, or might miss some cases.

Another reason TDD is good is because it forces you to think how your code will work and look like before implementing it, which would help you make better upfront design decisions—some find this true and some don't, but personally I find it useful in that regard.

TDD might not be for everyone. You don't have to use it if you don't like it. I think of it as one style of writing tests.

Tools for writing tests

I don't think the tool is the most important thing here. The JavaScript ecosystem has a good number of testing tools, like Jest, Mocha, or Vitest, which is what I use these days.

The above tools are mainly used for unit and integration testing. At the time of this writing, Cypress and Playwright are the most popular tools for end-to-end testing.

Any of these tools would do the work. The important thing here is to learn how to write good tests—and that only comes with practice.

Code coverage

Code coverage is the percentage of how much your code is covered with tests. Some aim for 100% coverage. I find this almost impossible to have; instead, you should aim to cover the critical, major parts of the behavior of your software.

For example, you don't have to test all the getters and setters of an object if they work as simple getters and setters. Instead, test the thing that really matters to how your software pieces should behave. For example, test a setter of an object if it runs some validation before setting a value.

So the question of how much coverage I should have in my tests is not easy to answer. It usually depends on what phase the software is in and what the software is really trying to do.

A simple example

My goal in this article is not to show you how to write tests (I might have more articles on these in the future), but rather to show you the whats and the whys of testing.

Having said that, I think it would be a good idea to show you a quick example of what testing looks like in the code.

In this simple example, I'll write a function that adds two numbers (the classical example of testing), and I'll write a test for it.

I'll use Vitest in this example, but you can use whatever tool you like.

You can check their docs to see how to install it. After you install it, create a new test file called calculator.test.js.

// calculator.test.js

import { describe, it, assert } from 'vitest'
import { add } from './calculator'

describe('calculator', () => {
  it('add two numbers', () => {
    assert.equal(add(2, 2), 4)
  })
})
Enter fullscreen mode Exit fullscreen mode

This code tests a function called add in a module called calculator. That function takes two arguments to add. So, it tests if adding 2 and 2 returns 4.

To make it pass, you have to create the calculator module—see how I wrote the test before the code, that's basically what TDD is.

// calculator.js

export function add(a, b) {
  return a + b
}
Enter fullscreen mode Exit fullscreen mode

Now, if you run the test it should pass.

You can check the docs of the tool to see how to run the tests. In my example with Vitest, I had to add a new script to package.json called test. And when I run npm run test, it would run all of my tests.

Conclusion

Testing is an important part of writing good code. With it, you can detect bugs and refactor your code. Not only that, but it also helps you think about how your software should behave, especially when you use TDD.

Learning how to write good tests only comes with practice. It's okay if you find it difficult to test something at first, but if you keep practicing, you will eventually find your own testing style and what works and what doesn't.


Thanks for Reading! Follow me on Twitter for web dev tips. Let's be friends!

Top comments (6)

Collapse
 
ant_f_dev profile image
Anthony Fung

Great article on testing!

I used Mocha when I learned to write tests in JavaScript. It was quite a while ago and have since noticed that Jest (also mentioned in the article) seems to be becoming more popular. Are there any features of Jest or Vitest that make them 'better' choices?

Collapse
 
tahazsh profile image
Taha Shashtari

Thanks, Anthony!

In my opinion, the tool doesn't matter. All of these tools are great and can do the job well.

However, there are small differences between them in terms of the developer experience. The main ones are:

  • To create mocks in mocha you need to install an additional library called sinon. Whereas in Jest you don't need to install any extra libraries—everything is included for you.
  • Vitest claims to be faster than Jest because of the hot module reload (HMR) it has.
  • Vitest integrates better than Jest with the workflow if Vite application.

So, I think it's better to use the tool you feel more comfortable and productive with.

Collapse
 
ant_f_dev profile image
Anthony Fung

Thanks for the high level overview Taha!

JavaScript wasn't my first programming language. I had come to front end development from doing server side work. I had used a library in C# called Moq to create mocks in my tests up until then.

When my team lead taught me about JS unit tests, he introduced me to using Mocha, and Chai for assertions. When I asked about how to verify that certain function calls had been made, he gave me a quick tutorial on Sinon and spies.

I didn't realise that Jest was a batteries-included library.

Good to know that they're all pretty much the same though - Thanks!

Collapse
 
zvone187 profile image
zvone187

Hi @tahazsh, great post. I'm happy when I see a post about test automation here since I rarely do. Since you're experienced with testing, I'm wondering if you could give me feedback on a project I recently open sourced (github.com/Pythagora-io/pythagora). It basically generates automated integration tests by analysing server activity so devs don't have to code the tests. What do you think, could you use these kinds of generated tests? Do you think I'm missing anything in terms of getting Pythagora to generate meaningful tests for devs. This is obviously without doing TDD.

Collapse
 
tahazsh profile image
Taha Shashtari • Edited

Thanks, @zvone187! I haven't tested it, but here's my opinion based on the demo video.

First, it's an interesting idea. I would consider these kind of tests more as end-to-end than integration. So the main use case I would use it for is quickly generate end-to-end tests to make sure all my endpoint works. But in my opinion, they shouldn't replace writing unit tests. Unit tests are still needed for testing modules at a lower level.

Another good use case is to use it for MVP projects, where the software developer is more concerned about getting the product out to get investors and feedback from users. In that phase, it would be great to have tests without spending time writing them (and a lot of companies don't write tests for these kind of projects).

Good job for building that project. And I wish you the best luck!

Collapse
 
zvone187 profile image
zvone187

Thank you so much for this feedback Taha and for the encouraging words, appreciate it! And yes, you're right, Pythagora doesn't replace unit tests (or manual tests for that matter) since they have different objectives and are definitely all needed.