DEV Community

Cover image for How to write unit tests
Marcin Wosinek
Marcin Wosinek

Posted on • Originally published at how-to.dev

How to write unit tests

As a beginner programmer, you often hear advice to test your code. It’s good advice—let’s look at how you can start doing it!

What are unit tests

Tests are a way to explicitly set expectations about code. You establish them to allow the machine to check whether your code meets the expectations.

It’s a program that verifies your program.

Usually, in JavaScript projects, you’ll use some testing library, such as:

  • Jest,
  • Jasmine, or
  • Chai

But those are just tools. What matters is that you have some way of automatically validating your application.

Image description

How unit tests help you

There are four ways writing tests will make your coding life easier:

  1. It’s a fast and reliable way of checking whether the code works as expected. You don’t have to think about edge cases to have all of them covered by unit tests.
  2. Good test coverage is a safety net that allows you to refactor code with more courage. Thus, you’re more likely to take the necessary steps to keep your codebase in good shape.
  3. Writing unit tests forces you to think about units—and how the responsibility should be spent between them, making your code more modular and easier to maintain.
  4. Unit tests can make you a faster coder. At first, you have to invest time in creating the test case, but once it’s ready, you can rerun it very cheaply. The investment can pay dividends even during the initial development.

Build up scaffolding

Before testing functionality, make sure you can test anything. Install the testing library and set up your testing script. Once you have something running, you can start setting up scaffolding for some of your tests. You need to decide on a naming convention. For example, if your code is my-project/plane-ticket.js, your testing code can be sitting in my-project/plane-ticket.spec.js.

Build everything needed to test a given class, and then check some trivial aspects:

  • if an object is an object, or
  • if a function is a function

In this way, you will prove that you can test things.

Set up mocking

Mocks are objects created to replace dependencies of the unit you are testing. For example, if you test the saveBlogPost function, you will want to intercept the HTTP request before the function sends it. You will want to find what your function uses for sending the request and replace it with a mock. Mocking should be easy if you build your code using a dependency injection pattern.

Image description

Keep a structure

As you can see, a lot is going on in each test. You can distinguish three main phases:

  1. Setting up mocks
  2. Running code you want to check
  3. Check expectations

Keeping this separation in your code makes sense; it will be easier to read this way. An easy way of organizing it is to group all lines together and maybe add a comment saying which part it is.

Test-driven development

Test-driven development is a common approach to creating nice code with good test coverage. You start with adding a test for a function before there is an implementation for it. You run tests, and it should fail—if not, there is something seriously wrong and you need to investigate it. The tests fail, and you add the missing implementation to the code. Again, the expectation is that this alone will fix the failure. If all goes well, you invest a bit of time in improving your solution—on both the code and test side, without changing the logic. This way allows you to iterate quickly in creating the code and its tests.

If you follow this practice, you should never miss any tests for your logic. There is no temptation to skip writing tests—a common issue when you leave writing tests at the end of your sprint.

Image description

Counter-recommendation

To lead, you have to know where you are going. Ignore the tests for a while if you need to explore what solutions are doable. Once you have your path clear, you can either add tests or approach the problem again in a test-driven way.

Missing tests

If you are unlucky, you might be working with legacy code without tests and any other quality-related measures—something like I describe here. In such a case, it’s still better late than never; you can start writing tests as you work on the codebase. In this way, you will be improving the situation for the future, and maybe you will find some nasty bugs hidden in some rare edge cases.

How about you?

How difficult do you find learning how to test? I’ve seen complaints online from people who struggle to find good resources for it. Let me know what experiences you have had so far.

Discussion (12)

Collapse
jonrandy profile image
Jon Randy • Edited on

I've used automated testing extremely rarely over 26 years being a professional developer. It's possible to get on just fine with manual testing

Collapse
peerreynders profile image
peerreynders

Testing is also about improving design.

The Flawed Theory Behind Unit Testing:

  • "One very common theory about unit testing is that quality comes from removing the errors that your tests catch. … It’s a nice theory, but it’s wrong."

  • "All of these techniques have been shown to increase quality. And, if we look closely we can see why: all of them force us to reflect on our code. When you write unit tests, … you scrutinize, you think, and often you prevent problems without even encountering a test failure."

  • "I like the fact that the tests we end up with using TDD give us additional leverage: they make it easier to change our code and know that it is still working without having to re-reason through the entirety of the code base."

  • "Quality is a function of thought and reflection - precise thought and reflection."

  1. Tests are supposed to enable "Refactoring Mercilessly"
  2. Code has to be structured in a certain way to be testable. That tends to drive designs towards loose coupling across internal boundaries around cohesive capabilities.

One caveat: Michael Feathers is a classicist (Detroit/Chicago style), not a mockist (London style); i.e.

"The problem that I saw with the mock object approach was that it only tested individual classes, not their interactions."

Which means overuse/abuse of mocks can undermine the beneficial design pressures that tests can bring to bear; typically mocks are OK for dependencies you don't control but a classicist would generally not mock code that is under their control.

Now people will often quote DHH's Test-induced design damage to support that TDD doesn't improve design. However that is from the perspective of a framework author. A framework (or library) imposes design pressures of it's own which often compete with those of the application domain.

With a framework a problem is always solved on the framework's terms somehow coercing the various application concerns in there. So TDD will always place frameworks (like Rails, React) with the Horrible Outside World.

Collapse
marcinwosinek profile image
Marcin Wosinek Author

Well said! Thanks for your comment.

Collapse
marcinwosinek profile image
Marcin Wosinek Author

I like a lot the reassurance that an extensive test suite gives me. In my main project, we have more than 3000 unit tests and about 400 E2E. I never worry about doing refactoring, library updates, weird coupling between code - if all automated tests passes, it's very improbably that anything is seriously broken.

Definitively you have a compound effect here - the longer your project lives, the longer you are getting benefits out of investment into writing tests. In my project, I wrote some E2E 6 years ago, and since then run them in the order of few tousands times.

Besides, writing or not writing tests is a decision on the project or team level. It would be frustraing and not productive to be the only developer caring about tests. In the oposit case, changes without tests simply wouldn't be aproved.

Collapse
jeremyf profile image
Jeremy Friesen

I like the language of "improbable that anything is seriously broken." It highlights the fact that our tests are useful in providing some degree of comfort but also that we shouldn't solely rely on them and assume they catch everything.

Automated tests are a long running conversation we have with our system, and as both change, drift inevitably occurs.

Collapse
skinnypetethegiraffe profile image
Bobby Plunkett • Edited on

I've been working on a testing-focused project for the past few years, where unit-testing is integral to the functionality of our application, and all benefits you mentioned above are true.

Our team uses the BDD testing approach over vanilla TDD, as we found more value in testing user flows than simply aiming for 100% coverage.

The way I see it, I can have 100 tests that cover every line or have 10 targeted tests that cover the flow users would take using the application. Coverage and flows are both important, and finding the balance between them is key.

Collapse
marcinwosinek profile image
Marcin Wosinek Author

Nice article, thanks for linking! I spend the last 8 years working with angular, I had DI already figured out for me by the framework:)

About "I should write more tests" - even though I'm complelty in favor of TDD & testing, I sometimes have to force myself to spend a time on them:) It's definivetly lest satisfying part of the job

Collapse
jeremyf profile image
Jeremy Friesen

How difficult do you find learning how to test?

When I first saw unit tests (Rails 1 or so) I chose to not learn both Rails and how to write tests at the same time. I chose to write Rails. That was a mistake. Had I focused on writing tests, I would've learned Rails along the way and had a better understanding of the implications.

These days, I often dive into unit tests to help me think through the problem. This involves mocks, stubs, and dependency injection (my preference).

So, it's a bit disheartening to hear that there aren't good resources for learning tests.

Collapse
marcinwosinek profile image
Marcin Wosinek Author

For learning unit tests I would try doing radical TDD for few weeks. Radical, as in not writing a single line of code, unless you have a test that fails when the line is not there. It will be weird at first - you will spend more time thinking about writing tests than about writing code. But pretty fast you will develop a sense of what is easy to test and what is not

Collapse
jeremyf profile image
Jeremy Friesen

I did that for a brief stint, and it really helped me develop that sense you mention.