testing – tsmx https://tsmx.net pragmatic IT Thu, 02 Feb 2023 19:22:23 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.2 https://tsmx.net/wp-content/uploads/2020/09/cropped-tsmx-klein_transparent-2-32x32.png testing – tsmx https://tsmx.net 32 32 Full code-coverage with Jest https://tsmx.net/jest-full-code-coverage/ Mon, 03 Jan 2022 22:03:13 +0000 https://tsmx.net/?p=1372 Read more]]> A guided tour on how to achieve the most complete code-coverage with Jest in your NodeJS projects and an some thoughts on why this is not necessarily the primary target.

In this article I assume you have a basic understanding of what Jest is and how it is used. So let’s directly jump to it getting our code covered.

Example function to test

In this tour we’ll use the following function as the test object.

// test-functions.js

module.exports.calculateValue = (valueA, valueB, addOffset = false, addWarning = true) => {
  let result = {
    value: (addOffset ? valueA + valueB + 10 : valueA + valueB),
    message: null
  }
  if (result.value > 100) {
    let messageParts = []
    if (addWarning) messageParts.push('Warning:');
    messageParts.push('result is greater then 100');
    result.message = messageParts.join(' ');
  }
  return result;
}

To follow the next steps save this function in test-functions.js in the main folder of your NodeJS project.

Covering your code with Jest

Initial setup and test function touch

To get Jest up & running, install it as a dev dependency in your project.

npm -i jest --save-dev

For ease of use, also in your CI/CD pipelines, I recommend to add script entries for running Jest with and without collecting coverage statistics in your package.json, like so…

...
"scripts": {
  "test": "jest",
  "test-coverage": "jest --coverage"
}
...

Having this in place, you can run the tests in your project by simply calling npm run test or npm run test-coverage (including gathering coverage stats).

Now let’s start writing the test suite for our sample function. We start with an empty suite saved as test/test-functions.test.js which is simply touching the source file…

// test/test-functions.test.js

const { calculateValue } = require('../test-functions');

describe('test-coverage test suite', () => {

  it('tests something', () => {
  });

});

Now, doing a first call to npm run test-coverage will show you that our source file is touched – also it has nearly no coverage yet.

jest-coverage-no-test

You may ask why we are writing this empty test suite at the beginning before showing the coverage stats? The answer is that Jest will – by default – only collect coverage stats for touched source files. Without this empty suite referencing test-functions.js, Jest would report everything green because there’s not even one source file affected, which is not intended here. Please read this great article on how to change that behaviour if you whish or need to in your project.

First straight-forward test

Now let’s write the first simple test in our suite. We’ll call the function to test with the two necessary parameters for valueA and valueB.

it('calculateValue test 1', () => {
  expect(calculateValue(10, 20).value).toBe(30);
});

Having this test in place, the stats are showing we’ve already covered quite some amount of our code…

jest-coverage-test-1

Great, so far. We covered the main part of our function but the test written is very poor. This is, because our function returns an object with two properties but in the test, we only check one if it. Such tests are prone to not-discovering side effects in later changes etc., although they may increase and polish the coverage…

So let’s refactor that being a better test.

it('calculateValue test 1', () => {
  const result = calculateValue(10, 20);
  expect(result.value).toBe(30);
  expect(result.message).toBe(null);
});

Although this refactoring didn’t change anything regarding the code-coverage, I think you will agree that the quality of the test increased a lot. So always remember: code-coverage is not necessarily a synonym for high quality!

Testing uncovered lines

After running our first test, the output of npm run test-coverage shows uncovered lines 9-12. They are marked red, because they are completely untouched by any test.

These lines are within an if-statement that is only entered when the passed two values together will be greater than 100. So let’s go ahead and write another simple test satisfying this condition.

it('calculateValue test 2', () => {
  const result = calculateValue(100, 20);
  expect(result.value).toBe(120);
  expect(result.message).toBe('Warning: result is greater then 100');
});

Having this second test in place, the stats are now showing a 100% coverage of all lines and statements.

jest-coverage-test-2

However, there are still uncovered lines (5 and 10) left marked yellow. So let’s go on in covering these.

Testing uncovered branches

The uncovered lines marked in yellow are part of code branches (if-else statements, ternary operators or else). Corrsponding to that you can see that our branch coverage is at 75% and therefore not complete yet.

So let’s start in inspecting first line, which is number 5:

value: (addOffset ? valueA + valueB + 10 : valueA + valueB),

Can you see what is not covered here by any test? Now, it might be a bit tricky – even more than in our example – to figure out what is not covered. Fortunately, Jest provides a great tool for further coverage inspection: a HTML report.

When running Jest with the --coverage option (like in our npm run test-coverage), it generates a report under coverage/lcov-report/index.html. Simply open this site iny our browser and navigate to our source file’s sub-page.

jest-coverage-html-report

Here – again marked yellow – you can very easily see, what part of line 5 is not covered by any test. It is the first return path of the ternary operator which would be executed if addOffset is true. Hovering the mouse over it will show you ‘branch not covered’.

So let’s write another test covering this code branch.

it('calculateValue test 3', () => {
  const result = calculateValue(100, 20, true);
  expect(result.value).toBe(130);
  expect(result.message).toBe('Warning: result is greater then 100');
});

Great! Line 5 is now fully covered.

jest-coverage-test-3

Nevertheless, line 10 is still left marked uncovered also the generated HTML report doesn’t show anything there?!

Testing non-existent else-paths

To solve this mystery, let’s investigate line 10:

if (addWarning) messageParts.push('Warning:');

Obviously, the condition for the if-statement is true for all of our existing tests as addWarning is true by default in the test function and we never did override that. Therefore, this line is already covered, BUT: there is an implicit else-branch not written explicitly in the code which is never taken. This is when addWarning is false. Then the code in the if-statement will not be executed which may affect the logic of our function and therefore is subject to test for Jest by default.

So let’s write another test covering the implicit else-branch of our test function.

it('calculateValue test 4', () => {
  const result = calculateValue(100, 20, false, false);
  expect(result.value).toBe(120);
  expect(result.message).toBe('result is greater then 100');
});

Awesome! We have now covered our test function to 100% in all aspects: statements, lines and also branches.

Excluding untestable code-sections from the coverage stats

Despite all attempts you might end up in not finding a feasible or “affordable” test case for every line of code. To keep your coverage stats clean and meaningful anyways, Jest provides a way to explicitly exclude code sections from the coverage stats. For that, please refer to the article on ignoring code for coverage.

Your primary goal should always be to cover a line/branch with an appropriate test. But not at any price.

Summary and further steps

Putting it all together, our final test suite for a full code coverage looks like this. Note that the empty initial test case from the setup is removed as this was only needed to do the initial run of the Jest suite (a suite must contain at least one test).

// test/test-functions.test.js

const { calculateValue } = require('../test-functions');

describe('test-coverage test suite', () => {

  it('calculateValue test 1', () => {
    const result = calculateValue(10, 20);
    expect(result.value).toBe(30);
    expect(result.message).toBe(null);
  });

  it('calculateValue test 2', () => {
    const result = calculateValue(100, 20);
    expect(result.value).toBe(120);
    expect(result.message).toBe('Warning: result is greater then 100');
  });

  it('calculateValue test 3', () => {
    const result = calculateValue(100, 20, true);
    expect(result.value).toBe(130);
    expect(result.message).toBe('Warning: result is greater then 100');
  });

  it('calculateValue test 4', () => {
    const result = calculateValue(100, 20, false, false);
    expect(result.value).toBe(120);
    expect(result.message).toBe('result is greater then 100');
  });

});

Where to go now? As the next step it is highly recommend to automate running the tests in your development process.

One very easy and convenient way to do so is GitHub actions. See my article on setting up a simple CI/CD pipeline with GitHub actions. There it is also explained how to include Coveralls for monitoring coverage stats.

100% code-coverage – worth the effort?

You may now ask yourself if it is always the ultimate target to reach 100% coverage? The answer to that is not easy and depending on many parameters of your project as you may have guessed 😉

In general, it is always a good approach trying to cover everything by following a pragmatic and affordable plan: writing straight forward test-cases, trying to get uncovered and non-existing branches. Normally, you should achieve quite high coverages of 80-90% or even more with that in a reasonable time.

Before you start investing another big amount of work for covering the rest up to 100%, keep in mind that…

  • As always in projects, the “Last Ten Percent” rule will kick in. It is very likely that you’ll spend more time in covering the last 10% then you did for the first 90%.
  • There are limitations by the test framework itself: Although Jest is a very popular and great framework, it is not fully free of technical errors. These might be preventing you to test properly or even produce false-negatives. At the time of writing this article Jest 27.4.5 was the latest version which had ~1.500 issues and 200 pull requests logged at npmjs.com.
  • You might have technically complicated edge-cases where you will spend more time in finding out how to implement the test itself rather then caring about the test logic. One example for that could be testing a process exit in a CLI project.

When reaching such points, you should always ask yourself: Is it beneficial for my project to spend more time here?

As a rule of thumb, I would recommend trying to reach an acceptable level (e.g. >= 80%) of code coverage by executing a pragmatic and straight-forward test implementation plan. Doing that, focus on complete and high-quality test cases where all pre- and post-conditions are checked to avoid side effects, especially for later changes. That’s more important then having full coverage. Then go ahead and don’t spend more time in implementing tests just for polishing coverage stats. Unless your employer or customer demand more or time & budget are unlimited in your project 😉

Useful links

]]>
Creating a time series sample CSV file with a bash script https://tsmx.net/dataseries-bash-script/ Fri, 26 Nov 2021 22:31:42 +0000 https://tsmx.net/?p=1326 Read more]]> Demonstrating a pragmatic way for generating time series sample data in CSV files with a bash script. Useful for Proof-of-Concepts, unit and performance testing and many other scenarios.

Time series are a widespread data structure for many use cases in different domains, e.g. stock or commodity prices, sensor data from IoT devices, measured values and so on. Getting required sample data for those values might be tricky in some situations. So let’s explore an option on how to solve that.

Basic time series structure

In general, a time series is simply a set of chronological values for a specified key. The key identifies the entity or process the given data is related to, e.g. the ID of a sensor, a share or a process step. Mostly, every time series has a fixed time granularity, meaning values are present for every minute, hour, week etc.

So a minimal time series data set usually consists of three fields: a key, a timestamp and a value. E.g. for a hourly granularity:

keytimestampvalue
sensor-12021-10-07 08:00:00110.07
sensor-12021-10-07 09:00:00112.10
sensor-12021-10-07 10:00:00111.37
sensor-12021-10-07 11:00:00109.98

The challenge of providing time series data

Providing time series data as a needed input will be a challenge in most non-production environments and situations where you don’t have any real world data available – for example in isolated environments, during a Proof-of-Concept or others.

Typically, when dealing with time series data we are talking about mass data where hundreds of thousands or millions of data sets are needed to create nearly realistic situations.

Luckily, in most scenarios it is absolutely sufficient if you would have synthetic sample data in a CSV file at your hand. To generate such files, a simple bash script can be used. So let’s get to it.

Bash-script for creating time-series data

To generate the needed time series sample data you could perfectly use any spreadsheet office app and export to CSV. But there are some things to keep in mind:

  • Typically, no office app is directly available on the target machine/server, so extra effort for connecting, copying files etc. will kick in.
  • Since we are talking about mass data, most spreadsheet office apps will come to their limits quite fast (max. number of lines…).

Taking all this into account, a more efficient way would be to create the sample data for your time series directly on the target machine. Before getting into the details, here’s the script that can serve you as a good basis to achieve this.

#!/bin/bash

declare -a timeseries=( "TS1|50|100" "TS2|100|120" "TS3|5|15" "TS4|10000|13000" )
amount=$((10*365*24))
start="06:00:00 2017-01-01"

create_ts_values () {
  i=0
  while [[ $i -lt $amount ]]
  do
    echo -ne "$1... ${i}"\\r
    echo "$1;$(date -d"$start +${i} hours" +"%Y-%m-%d %H:%M:%S");$(($2 + $RANDOM % ($3-$2)))" >> ts.csv
    i=$((i+1))
  done
}

rm -f ts.csv

echo "Generating timeseries values..."
for val in ${timeseries[@]}; do
  ts=$(echo $val | cut -d '|' -f 1) 
  from=$(echo $val | cut -d '|' -f 2)
  to=$(echo $val | cut -d '|' -f 3)
  create_ts_values $ts $from $to
  echo "$ts... $amount"
done
echo "Done."

Feel free to copy the script code or download it from here.

Now, what does this script do? It will create a file ts.csv containing random hourly time series values for ten years (= 10 * 365 * 24 values) starting from 01-01-2017 06:00:00 for four series with the following ranges:

  • Key: TS1, value range: 50..100
  • Key: TS2, value range: 100..120
  • Key: TS3, value range: 5..15
  • Key: TS4, value range: 10.000..13.000

In total, the resulting CSV file contains 350.400 entries.

$ head ts.csv 
TS1;2017-01-01 06:00:00;65
TS1;2017-01-01 07:00:00;62
TS1;2017-01-01 08:00:00;88
TS1;2017-01-01 09:00:00;83
TS1;2017-01-01 10:00:00;69
TS1;2017-01-01 11:00:00;50
TS1;2017-01-01 12:00:00;69
TS1;2017-01-01 13:00:00;79
TS1;2017-01-01 14:00:00;99
TS1;2017-01-01 15:00:00;85

The resulting file is a perfect starting point for importing into popular databases, e.g.:

Understanding and customizing the script

Time series keys and ranges

The time series to generate the sample values for are specified in the array timeseries.

declare -a timeseries=( "TS1|50|100" "TS2|100|120" "TS3|5|15" "TS4|10000|13000" )

For each series a tuple of key, range-from and range-to delimited by pipes (‘|’) is present. So "TS1|50|100" means “create a series with key “TS1″ and random values between 50 and 100”.

To change/add/remove time series, simply edit the array definition.

Changing time granularity and/or generated fields

The given script generates sample data on an hourly basis. To change this or if you have to add/change fields of the resulting CSV, edit the following line that produces the output.

echo "\"$1\";$(date -d"$start +${i} hours" +"%Y-%m-%d %H:%M:%S");$(($2 + $RANDOM % ($3-$2)))" >> ts.csv

For example, to switch to daily values this line would have to be changed to this.

echo "\"$1\";$(date -d"$start +${i} days" +"%Y-%m-%d %H:%M:%S");$(($2 + $RANDOM % ($3-$2)))" >> ts.csv

For manipulating the date and time, the linux command date is used. To learn about all the features please reach out to man date and also info date for all possible string formats.

Happy coding 🙂

Useful links

]]>
Testing process.exit with Jest in NodeJS https://tsmx.net/jest-process-exit/ Fri, 17 Sep 2021 19:52:29 +0000 https://tsmx.net/?p=1000 Read more]]> Demonstrating a pragmatic way on how to test code paths resulting in process.exit using the Jest test library.

The test scenario

With NodeJS, Jest is a very popular and powerful testing library. Consider you want to test the following function in your project…

function myFunc() {
  //
  // ...do "stuff"
  //
  if (condition) {
      process.exit(ERROR_CODE);
  }
  //
  // ...do "other stuff"
  //
 }

To reach a full test coverage, you’ll need to set up a test for the branch where condition is true. Setting up such a test in Jest without any precautions would result in a real exit of the test process before it is finished wich would cause a failed test.

Mocking process.exit

To safely test the branch where condition is true you have to mock process.exit. Of course this mocking should have been done in a way that the following code “other stuff” is never executed like if the original process.exit would kick in.

To achieve that, we use Jest’s spyOn to implement a mock for process.exit. The mock method introduced with mockImplementation will throw an exception and replace the original implementation which was exiting the entire process. The mock function will receive a number (the error code) as argument like the original exit function.

const mockExit = jest.spyOn(process, 'exit')
  .mockImplementation((number) => { throw new Error('process.exit: ' + number); });

This mock ensures that the execution of our test function ends immediately without doing “other stuff” and without ending the Jest test process. Also, this mock serves to check if process.exit was really called and what the exit code was. We do this with Jest’s toHaveBeenCalledWith test function.

Putting it all together

To get the test case finally up and running, we have to wrap our function execution in an expect( ... ).toThrow() statement because it is now throwing an exception in the mock implementation. Also, it is a good practice to restore the original mocked function by calling mockRestore to avoid unintended side-effects.

Assuming we want to test a process exit code of -1, our final test case would look like this…

it('tests myFunc with process.exit', async () => {
  const mockExit = jest.spyOn(process, 'exit')
      .mockImplementation((number) => { throw new Error('process.exit: ' + number); });
  expect(() => {
      myFunc(true);
  }).toThrow();
  expect(mockExit).toHaveBeenCalledWith(-1);
  mockExit.mockRestore();
});

The complete example code is available in a GitHub repo.

Happy coding 🙂

Useful links

]]>
CI/CD with GitHub actions for NodeJS with Coveralls https://tsmx.net/ci-cd-with-github-actions-for-nodejs/ Sun, 13 Dec 2020 20:40:02 +0000 https://tsmx.net/?p=523 Read more]]> 10-minutes setup guide for building a NodeJS repo including Coveralls test stats. No need for Travis any more.

Importance of CI for small NodeJS projects

CI/CD nowadays is an integral part of every medium to large-scaled software project to be successful. For small and very small projects, e.g. your private ones with you as the only developer, you may ask yourself if it does make sense to setup a CI process and bear all the efforts for that.

I was also wondering a long time about that question, but the clear answer to that is YES. Because:

  • It is much less work then you’ll eventually expect – learn in this article how a basic CI process is setup in around 10 minutes one-time effort for your NodeJS project.
  • It takes your projects quality and professionalism to a complete new level.
  • It enables you to create badges for build success and code coverage of your tests. Very nice for README’s and publishing on npmjs.com.
  • You can have it for free at GitHub.

Used Travis-CI for free so far? Use this guide to migrate.

Starting some time ago using CI for small NodeJS projects, Travis-CI was a perfect platform for doing that at no cost. Unfortunately, Travis-CI decided in late 2020 to change the product and pricing model… in a very bad way for a single non-commercial developer having only a couple of repositories.

So read on to get to know how to easily use GitHub actions instead… 😉

Setting up a minimal NodeJS CI workflow with GitHub actions and Coveralls

To use a GitHub action for building your repository at each push (on all branches), all you have to do is to place a YAML file under .github/workflows and commit it with your repo.

Update September 2022: GitHub bumped NodeJS from 12 to 16 for running actions. So make sure to use the appropriate versions v3 of the checkout and the setup-node action. Also you should use at least NodeJS 16 as basis for your own workflow.

# save as ./github/workflows/git-build.yml
# make sure that 'test-coverage' generates the coverage reports (lcov)

name: git-build

on:
  [push]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: '16.16.0'
    - run: npm ci
    - run: npm run test-coverage
    - name: Coveralls
      uses: coverallsapp/github-action@master
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}

That’s it! Commit & push the workflow definition and head over to the Actions section of your repo where you can see all executed workflows…

…and detailed results for every step by clicking on the workflow…

Easy, right? With the workflow definition above the following job will be executed on each push in every branch of the project:

  • Checking out the latest repo version that was pushed
  • Installing NodeJs 16.16.0
  • Running npm ci to install all dependencies
  • Running npm test-coverage
  • Transferring the test results to Coveralls using Coveralls GitHub Action

For further information e.g. on how to build on multiple versions of NodeJS there`s a very detailed documentation on GitHub.

Some sidenotes about CI with GitHub actions:

  • Use npm ci rather than npm install to install your dependencies in a CI process. Refer to the npm-ci docs to get to know about why this is preferred.
  • For authentication to third-party actions/plugins e.g. like Coveralls in our example, GitHub by default provides the GITHUB_TOKEN secret in every workflow. You have nothing to do, just use it in your definition. For more details refer to the workflow authentication docs.

Building and testing multiple NodeJS versions with one action

Sometimes it benefical to build and test your software with multiple versions of NodeJS, e.g. to cover backward compatibility or to test against all available LTS releases.

To achieve that, GitHub actions provides the feature to specify multiple NodeJS versions in one single action specification with the strategy and matrix elements.

# save as ./github/workflows/git-build.yml
# make sure that 'test-coverage' generates the coverage reports (lcov)

name: git-build

on:
  [push]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x, 18.x]

    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run test-coverage
    - name: Coveralls
      uses: coverallsapp/github-action@master
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}

On pushing into the repo, GitHub actions will now execute 2 jobs for the specified NodeJS versions.

Commercial review of the solution

Setting up a CI build with GitHub actions is very easy – now what about the costs? Good news on that…

  • GitHub actions is completely free for every public repository!
  • The free GitHub plan already includes 2,000 action minutes per month for private repos.

That’s fair. In comparison Travis CI was also free for public repos in the past but there wasn’t any free contingent for any private repo. Building even only one private repo you would have starting paying at Travis…

And even if the 2,000 minutes/month are not enough for you the GitHub plans seem very affordable compared to other CI service providers.

The next stage would cost you only 4$/month if you are a single developer. Pretty ok, right.

To easily keep track of your consumed actions minutes simply head to Billing&plans under your setting at GitHub. There you can also set up a spending limit if you’ve already provided a payment method.

Useful links

]]>