The Stroz

Verifying Code Coverage With Karma

Mar 24, 2022
7 minutes

JavaScript TDD Karma Jasmine Better Tests

This is the first in a series of posts that will help ease the transition to TDD and how we can make sure we write tests that actually verify the code we write is correct.

What is coverage?

In this post, we are going to talk about something that is probably more useful if you already have code that you want to write tests for - ‘coverage’. Put simply, checking ‘coverage’ means we analyze our code and verify that every bit of it is covered by tests. That may seem like a daunting task, but many test runners offer this feature and make it easier to accomplish.

The sample code for this post can be found on GitHub and uses the Karma test runner and Jasmine testing framework. I will assume you already executed npm install.

Inspecting the Configuration File

When starting fresh, Karma can get you started with setting up a configuration file. Simply execute the following command:

karma init { filename }

Follow the prompts, provide the requested information (or don’t - you can always edit the file later), and Karma will generate a configuration file for you with the name you provide. We already have a configuration file named my.conf.js. Let’s look at some parts of that file.

The first important bit of information is the framework property. In our file it looks like this:

frameworks: ["jasmine"];

This simply tells Karma we are using the Jasmine testing framework. Support for this was added to your project when you ran npm install.

Next, we tell Karma where to find our files.

files: ["js/*.js", "tests/*.js"];

This tells Karma where to find the files we need. This should include paths to all the JavaScript files we want to test as well as all of our test files. We are telling Karma to include all JavaScript files in the /js directory and the /tests directory. You can view more information about the files property in the documentation.

Further down the file, we see a property named preprocessors.

preprocessors: {
  'js/*.js' : [ 'coverage' ]
}

With Karma we can use preprocessors to do something with files before they are served to the browser (more on that in a bit). Here we are using the coverage preprocessor. For more information on preprocessors, checkout the Karma documentation.

The next block of code serves a few purposes.

reporters: ['progress', 'coverage'],
coverageReporter: {
  type: 'text'
}

The reporters property tells Karma how to report the results on the tests. In our example we are using progress and coverage. By using progress Karma will show the number of tests that are executed, how many that fail.

The coverageReporter property tells Karma how to prepare the results of the coverage report. In our case, we are using text as the type, so we will see the results in the command window when we run our tests. For more information on configuring the Coverage preprocessor, check out the documentation.

The last bit of configuration that is of interest is the browsers property.

browsers: ["ChromeHeadless"];

Karma runs our tests in a browser and this is simply telling Karma what browser to run them in. I prefer using headless Chrome so that my desktop is not cluttered with more windows. You can find out more information about what browsers are supported by checking out the docs for the browsers property.

Running The Tests

Now that we have our configuration file and understand what some parts represent, let’s run the tests and see what happens.

In your command window, execute the following command:

karma start my.conf.js

Karma will start up and run the tests we have set up in the /tests directory. When the tests are done, you should see something that looks like the following image.

Coverage results 1

That shows a lot of red, but this was by design. Let’s break down what we are looking at.

Dissecting the Results

The red arrows in the image above indicate how many tests were run and how many passed. A failure means that we received an unexpected result or there was an error.

Here are what the headers for the table represent:

  • File - shows the files that were analyzed, and we also see an aggregate for all files in the top row
  • % Stmts - tells us what percentage of statements are covered by tests.
  • % Branch - indicates the percentage of test coverage in the current branch of VCS (In our example, in this branch of Git)
  • % Funcs - displays the percentage of functions that are covered by our tests
  • % Lines - represents the percentage of lines of code that are covered by our tests
  • Uncovered Line #s - reveals specific lines that are not covered by our tests.

The difference between statements and lines of code can be summed up with a very simple example:

let i = 0; console.log(i);

This is 2 statements but only 1 line.

Getting the Tests to Behave

As you can see, we currently do not have a lot of coverage for our existing code. Let’s change that. Open up the file named my-code-spec.js and take a peak.

The second test we are running looks like this:

it("testing doubleNumber()", function () {
  let testNumber = Math.floor(Math.random()) * 10000;
  //let testResult = demo.doubleNumber( testNumber )
  //expect( testResult ).toEqual( testNumber * 2 )
});

As you can probably guess, the reason why the doubleNumber() function does not have any test coverage is because the call to that method is commented out. If we remove those comments, we should see different results in our coverage report.

it("testing doubleNumber()", function () {
  let testNumber = Math.floor(Math.random()) * 10000;
  let testResult = demo.doubleNumber(testNumber);
  expect(testResult).toEqual(testNumber * 2);
});

Karma should re-run the tests automatically, and you should see something similar to the image below.

Coverage results 2

w00t!! We now have some better coverage on our tests as 50% of our statements, functions, and lines are covered by our tests. If you uncomment other lines of code that call methods on the demo object, you will see the results of the coverage report change.

Let’s uncomment all of these except the last test, so our test file looks like this:

describe("My Code tests", function () {
  var demo;
  beforeEach(function () {
    demo = new TestDemo();
  });
  it("was initiated", function () {
    expect(demo).not.toBeNull();
  });
  it("testing doubleNumber()", function () {
    let testNumber = Math.floor(Math.random()) * 10000;
    let testResult = demo.doubleNumber(testNumber);
    expect(testResult).toEqual(testNumber * 2);
  });
  it("testing multiplyNumbers()", function () {
    let number1 = Math.floor(Math.random()) * 10000;
    let number2 = Math.floor(Math.random()) * 10000;
    let testResult = demo.multiplyNumbers(number1, number2);
    expect(testResult).toEqual(number1 * number2);
  });
  describe("testing divideNumbers()", function () {
    it("Can divide valid numbers", function () {
      let number1 = Math.floor(Math.random()) * 10000;
      let number2 = Math.floor(Math.random()) * 10000;
      let testResult = demo.divideNumbers(number1, number2);
      expect(testResult).toEqual(number1 / number2);
    });
    it("Cannot divide by 0", function () {
      let number1 = Math.floor(Math.random()) * 10000;
      //let testResult = demo.divideNumbers( number1, 0 );
      //expect( testResult ).toEqual( NaN );
    });
  });
});

When the test are run again, we should see this result.

Coverage results 3

You may be asking, how can we have 100% coverage of function, but not lines or statements? The answer is because in one of our methods, divideNumbers(), we have an if() statement and none of our tests trigger the else portion of that statement.

this.divideNumbers = (a, b) => {
  if (b !== 0) {
    return a / b;
  } else {
    return NaN;
  }
};

This feature of coverage is extremely helpful when you have complex nested logic inside a function as it helps you easily identify some conditions you may have missed.

Uncomment out those last 2 lines of code, and you should see the following indicating we have full test coverage.

Coverage results 4

Wrapping Up

The concept of ‘coverage’ is not unique to Karma. Other test runners, such as Jest, also have this functionality. Using this feature is a great way to help you start to testing your code. It is also invaluable when you need to test code that is complex and/or has several layers of nested logic.

If you did not fully understand the syntax of some test code, fear not, I will have more on that in the near future.

Photo by Medena Rosa on Unsplash