DEV Community

Cover image for Introduction to unit testing in JS
Arek Nawo
Arek Nawo

Posted on • Originally published at areknawo.com

Introduction to unit testing in JS

This post is taken from my blog so be sure to check it out for more up-to-date content 😉

If you're new to programming or you're just a hobby programmer and haven't ever done any kind of open source project then you might feel a bit skeptical about the idea of testing your code. If you indeed are, then in this article, let me convince you that doing unit tests for your code and testing it, in general, is a good practice to follow. Then we'll learn/revise a bit about code testing and finally explore best tools to do unit tests. Enjoy! 😉

Why testing?

At the beginning let's talk about why testing is important. When you're developing any kind of app or tool (especially open-source) then tests should be your allies. Consider the quality that they bring. When your tests cover all possible exceptions and cases in your code, you can be sure that it won't fail you in the future. That's yet another reason for testing - assurance. Having tests that cover your entire code-base kept up-to-date allows you to continuously check it for any errors. It makes you sure that everything is fine. If you haven't done any project that needs to be managed by a number of people or needs to serve others (OSS) then you might not really take this whole assurance as a thing. But believe me, it's really important. You can never be sure of wellness of your code without any warranty. 😕 Last but not least we have the advantage of documentation. Believe it or not, properly done tests can sometimes deliver an even better understanding of how to use a particular piece of code than the entire page of text. Just think about it. You have tests that your code passes. This way you have information about how to use the given code and what its outcome is. So, as you can see there are many reasons to start testing your code, thus if you haven't already, it's time to do some testing!

Kinds of tests

If I managed to convince you to do testing, then I'm really happy. 🎉 But the size of the world of code testing can feel a bit overwhelming at the beginning. There are so many terms, concepts, ideologies and tools, and libraries to know about. Let's give it some structure then. First things first, you have to know what kind of tests you're doing, so that you can later choose the right tools for the job. There are 3 main types of tests, divided by the reason they exist.

  • Unit tests - Unit testing allows you to test a really specific aspect of your code, e.g. one function against expected outcome. Here it's really important for your tests to cover all the code that you have, feature by feature. These are the main focal point of this post.
  • Integration tests - Test different parts of your code e.g. components to work as they should. You should also check the way they work together i.e. structural integrity. Side effects are also really important to check. You have to know if there aren't any functions calls and etc. that you didn't plan.
  • Functional tests - I think the name UI tests explains the purpose of these a little better. With functional tests, you check your final product e.g. web app in a specified environment, usually a browser. Here comes the concept of headless browsers, where you execute your test in a browser without visible UI by controlling it with different API calls. It might seem a little awkward at first, but it's a very useful technique, especially for saving some time required by UI and other processes, that are not present in headless mode.

Terminology

Now, that you know a bit more about different kinds of test and what unit tests exactly are, I think it's a good idea to talk a little bit about basic concepts and terms when it comes to testing.

I'd like to start by explaining the TDD and BDD shortcuts that you might have already seen somewhere before but didn't pay much attention to. As these can be seen as basic guidelines when structuring and writing your tests.

Test-Driven-Development (or TDD for short) is a process of developing your software while basing on tests. It's like a cycle, a loop - every time you want to add a feature, you first write your tests (which will obviously fail at this point), then you write the actual code, that fulfills these tests, and then you test again to check that. Your development is based on tests. Pretty interesting idea, won't you agree?

As for Behavior-Driven-Development (BDD), it's yet another ideology, which in fact is based upon TDD. But, the name might not be as self-explanatory as it was with the first one. It can be seen as TDD with some better, additional guidelines. Here, our development is driven not by test specifically, but by behavior, specification, which in fact are tests anyway. 😂 They are just better described, using pure English. This allows your tests to be much better documented and thus more readable. That's why libraries adopt this way of doing tests more often than TDD.

With the knowledge of these 2 trends that are so important when it comes to testing, it's time to explore some terms, explained just as BDD suggests, in English. 😉

  • Assertion functions - functions that we use to test our code against expected output. In Jest and many other unit testing libraries they look like this:
expect(value).toBeTruthy()
Enter fullscreen mode Exit fullscreen mode
  • Code coverage - indicate how big part of our code the tests cover. These are some incredibly useful stats, which can be an additional feature to consider when choosing your next testing library of choice. Of course, there are standalone tools for this, but having everything in one bundle is much more comfortable.
  • Environment - generally a (headless) browser or something similar for your functional tests. We're not going to dig into this one, but there are some nice options available here as well. 😄
  • Mocks - also called fakes, are used to fake certain behaviors to later use them in tests, to provide reliable input/output checks even without different features implemented in a realistic manner.
  • Spies - provide information about functions. By using them you can know e.g. how many times, when and with what arguments a function was called. They are possibly the best way to check for side effects.
  • Stubs - alternatively called dubs, give you the ability to replace chosen function with something different, to test expected functionality and behavior.
  • Snapshot testing - a way of testing, based on comparing output data with an already saved copy (snapshot).

With this, you should have a basic understanding of ways of testing and functionalities that you would expect from your library-of-choice. As here I'm focusing only on unit tests, be sure to check out some links in above-listed terms for tools that provide given functionality in a standalone package.

Unit testing

With theory out of the way, it's time to do some testing - unit testing! But first, let's choose the best tool for the job. Here comes the list of some of the best libraries and tools for unit testing & more. 🔥

img

Jest

test('adds 1 + 2 to equal 3', () => {
  expect(1 + 2).toBe(3);
});
Enter fullscreen mode Exit fullscreen mode

Jest is my personal go-to when it comes to unit testing. Started by guys from Facebook, it has been well battle-tested with a number of popular libraries, such as React itself. It provides almost all features required for high-quality unit testing. Readable assertion functions, great coverage reports, mocking API, parallel test runner and general ease-of-use makes this library one of the best choices available on the market right now, especially for BDD. Aside from that, a great community and support and well-written documentation are very much noticeable.

img

Mocha

var assert = require('assert');
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal([1,2,3].indexOf(4), -1);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Mocha is yet another library, with the goal of making testing fun and simple. Following BDD ideology, it has well-design descriptive API. Also, Mocha is different when it comes to its architecture. It provides developers with minimal, flexible setup, allowing them to extend it with other custom libraries, responsible for different tasks. With Mocha, you can use any assertion library you want (it doesn't have its own), including the NodeJS built-in one or Chai. Its API might feel a little similar to Jest's with some small differences. Because of its architecture, Mocha lacks features that Jest has built-in. We're talking about code coverage and more importantly parallel test runner (Mocha runs tests on one process only). Well, where it lacks in functionality, Mocha makes up in visuals with a great choice of tests progress reporters (a feature supported by Jest too). But, as I said, Mocha is for those you like to have their own configurable setups. Besides that, its docs might feel less polished that Jest's but they defiantly exhaust the topic.

img

Jasmine

describe("A suite is just a function", function() {
  var a;
  it("and so is a spec", function() {
    a = true;
    expect(a).toBe(true);
  });
});
Enter fullscreen mode Exit fullscreen mode

Jasmine might be a bit older than some of its competitors on this list. It's advertised as batteries-included, trying to provide every feature that developers would possibly need. Thus, Jasmine has assertion functionality built-in with expect style implementation. With that comes other built-in features, such as spies, mocks, reporters etc. Also, if you're doing some development with Ruby or Python, you might find it comfortable to use the same library, as Jasmine has official support for those two. As for the docs, they cover all the topics well, but their structure didn't really impress me.

img

Tape

var test = require('tape');

test('timing test', function (t) {
    t.plan(1);
    var start = Date.now();

    setTimeout(function () {
        t.equal(Date.now() - start, 100);
    }, 100);
});
Enter fullscreen mode Exit fullscreen mode

Tape is, again, minimal and flexible library for doing tests for NodeJS and browser. Its API is a bit different than the others' but still readable, with the same ideology in mind. Everything you need to know about it has its place in a single README file. And... it has pretty support for a great number of TAP reporters which is always an advantage.

img

AVA

import test from 'ava';

test('foo', t => {
    t.pass();
});

test('bar', async t => {
    const bar = Promise.resolve('bar');
    t.is(await bar, 'bar');
});
Enter fullscreen mode Exit fullscreen mode

AVA is a testing library with built-in assertion functions and great focus on async tests. It has simple API (just like other BDD tools) and the ability to run tests in parallel. Just like Tape (which it's inspired by), it has no implicit globals. In addition to that, it has Babel v7 built-in, so that you can write your tests in ES-Next without any additional configuration. All its documentation can be found on its GitHub repo.

img

Intern

Intern is a TDD/BDD testing framework and tests runner for JavaScript, as well as TypeScript. It allows you to perform both unit and functional tests. It uses Chai as the assertion library and Istanbul to generate your code coverage stats. It can also run your tests concurrently. Generally speaking, it's an extendable framework for doing tests. Did I mention that it has very good, comprehensive documentation?

img

Cucumber.js

Feature: Simple maths
  In order to do maths
  As a developer
  I want to increment variables

  Scenario: easy maths
    Given a variable set to 1
    When I increment the variable by 1
    Then the variable should contain 2

  Scenario Outline: much more complex stuff
    Given a variable set to <var>
    When I increment the variable by <increment>
    Then the variable should contain <result>

    Examples:
      | var | increment | result |
      | 100 |         5 |    105 |
      |  99 |      1234 |   1333 |
      |  12 |         5 |     18 |
Enter fullscreen mode Exit fullscreen mode

Cucumber.js is another unit testing tool, but this time a little different... It allows you to write your tests in plain language. Basically what you do is you write your test in a structured text format using some keywords like Scenario, Given, When etc. and then in your code you define what each step written in plain language should do to run expected tests. I haven't ever used such an approach, so I won't tell you how comfortable it is in real use. But, at least it seems interesting. 😅

Testing, testing...

So, that's it for my list of best tools for doing unit tests. Of course, if I missed an entry worth including in this list, let me know about that down in the comments. Keep in mind that I've covered unit testing only in this list, so no tools for integration, functional or end-to-end (covering every type of tests) testing here. I think it would be too much for one article. It's better to explore smaller topics one-by-one IMHO. In fact, by doing that article I learned a lot too. I'm definitely not a testing expert, but at least now I've got some new knowledge. I hope this post also helped you in your journey with code tests. That's all for now. If you like that article be sure to sometimes check my blog, also follow me on Twitter and on my Facebook page. ✌

Top comments (5)

Collapse
 
eljayadobe profile image
Eljay-Adobe • Edited

Cucumber uses Gherkin as its BDD language.

I think Gherkin does a good job, because BDD scenarios should be written by the product owner, and the goal of them is to represent both a formal requirement and an acceptance test in the given-when-then structure. Most product owners are not programmers, so having a BDD language expressed as some form of "embedded domain specific language" is an enormous barrier to entry.

Note: Gherkin isn't "free". The developers still need to create the infrastructure layer to support the nouns, verbs, and query the results of the actions which Gherkin depends upon.

I've seen BDD go awry because instead of being used by the product owner, they are (ab)used by the quality engineers to drive system tests or integration tests, or the software engineers to be (ab)used to express unit tests.

Whereas, TDD unit tests are not written by product owners, they are written by developers. They are not reflective of formal requirements, they are reflective of basic correctness. Their primary purpose it to design better code -- so they are written by developers as an aid for development, which puts strong pressure on the developer to leverage SOLID principles. A residual value is the byproduct of having a suite of unit tests to ensure basic correctness against regressions.

Using something like, say FitNesse, the full suite of requirements/acceptance tests take hours to run.

In contrast, a full suite of TDD-style unit tests should take second to run.

When a Gherkin test fails, one can say "uh oh, something in this general area is broken."

When a TDD-style unit test fails, one can say "the part of the code that does Xyz, in this exact Abc function, is broken". (Unless the test itself is broken, which sometimes happens.)

Collapse
 
gugadev profile image
Gustavo Garsaky

Good reading. I use Jest all time, except for Angular. Currently I'm building a UI library for a design system using Web Components (lit-element) but I don't have experience testing this kind of components. Any suggestion? Thanks.

Collapse
 
areknawo profile image
Arek Nawo

For this special use case, I think Mocha will be the best choice. Mainly because of its flexibility and extendability. Also, maybe this can be helpful. 😀

Collapse
 
gugadev profile image
Gustavo Garsaky

Yeah. I ended up with Karma and Mocha. Thanks!

Collapse
 
mfaghfoory profile image
Meysam Faghfouri

great and useful article. thanks