DEV Community

Chetan Patil
Chetan Patil

Posted on • Updated on

Level Up Your Node.js Testing with Native Test Runner and Mocks: A BigQuery Example

Code and Test

In today's software development, testing is not just good practice, it's necessary, in my opinion.

I'm excited to share some valuable insights on testing in Node.js! Through my own experiences, I've discovered some practical tips and techniques that I believe will be helpful to the developer community.

Before Node.js 20, developers relied on external testing tools like Jest and Mocha to ensure code quality. But with the arrival of Node.js 20, the game changed. This tutorial will explore the exciting new world of Node.js's built-in test runner, exploring powerful features like mocks.

Native Node.Js Test Runner Offers:

  • Simplicity (Easy setup, no extra depenndency)
  • Integration (Seamless integration with Node.js features)
  • Efficiency (Fast execution)

"Node.js's native test runner simplifies ESM testing and mocking, making it a better choice over Jest for modern projects."


From Theory to Practice: Mocking BigQuery Calls in Node.Js

We covered fundamentals of Node.Js native test runner, now let's get our hands dirty with real world scenario.

In this practical example, we'll walk through how to leverage the node:test module and its mocking capabilities to thoroughly test a Node.js module responsible for interacting with Google BigQuery. We'll focus on the @google-cloud/bigquery library and demonstrate how to simulate its behavior, ensuring our code is resilient and functions correctly without making any real BigQuery calls during testing.

Creating BigQuery Service

Let's assume to have task about querying data from BigQuery dataset in stream mode.

import { BigQuery } from '@google-cloud/bigquery';

export const createBigQueryClient = () => new BigQuery();

const query = async ({ query, emailId, country }, bigquery = createBigQueryClient()) => {
  const output = [];

  const options = {
    query: query,
    params: {
      emailId,
      country,
    },
  };

  return new Promise((resolve, reject) => {
    bigquery.createQueryStream(options)
      .on('error', reject)
      .on('data', (row) => {
        output.push(row);
      })
      .on('end', () => {
        resolve(output); // Resolve the promise when the stream ends
      });
  });
}

export {
  query,
};
Enter fullscreen mode Exit fullscreen mode

In this code, you must have observed export for createBigQueryClient.

Why Export createBigQueryClient?

Wondering why there is such simple function export:

export const createBigQueryClient = () => new BigQuery();
Enter fullscreen mode Exit fullscreen mode

Here's the reasoning:

Testability: This function is key to easily mocking the BigQuery client in tests. By abstracting the creation of the client, it becomes easy to swap it out for a mock during testing.

Potential Configuration: While not used in this example, imagine needing to pass authentication option or other setting. Having this function makes such future cases easy for implementation without changing core login for query function.

Soften BigQuery with Mocks: The Tests

Testing bigquery.js service directly would require live BigQuery project running, which is costly and not always controllable. Here mocking comes into picture and Node.Js native test runner gives us that functionality in simplest way.

The Power of node:test

import assert from 'node:assert';
import { describe, it, beforeEach, after, mock } from 'node:test';
import { query } from './bigquery.js';

describe('BigQuery Module', () => {
  let mockBigQuery;

  beforeEach(() => {
    mockBigQuery = {
      createQueryStream: mock.fn(() => ({
        on: mock.fn(function (event, callback) {
          if (event === 'error' && mockBigQuery.error) {
            // @ts-ignore
            callback(mockBigQuery.error);
          } else if (event === 'data' && mockBigQuery.data) {
            mockBigQuery.data.forEach(callback); // Stream sends individual objects
          } else if (event === 'end') {
            // @ts-ignore
            callback();
          }
          return this; // Return 'this' for chaining

        }),
      })
      ),
    };
  });

  after(() => {
    mock.restoreAll();
  });

  const tests = {
    success: [
      {
        name: 'should execute a query and return results',
        input: {
          query: 'SELECT * FROM dataset.table WHERE emailid = @emailid AND country = @country',
          emailid: 'chetan@example.com',
          country: 'IN'
        },
        mockData: [{ id: 1, name: 'Chetan' }],
        expectedOutput: [{ id: 1, name: 'Chetan' }],
        expectedError: null,
      },
      {
        name: 'should handle empty result set',
        input: {
          query: 'SELECT * FROM `project.dataset.table` WHERE 1=2', // Always false condition
          emailid: 'random@example.com',
          country: 'CN',
        },
        mockData: [],
        expectedOutput: [],
        expectedError: null,
      },
    ],
    error: [
      {
        name: 'should reject the promise on BigQuery error',
        input: {
          query: 'INVALID SQL QUERY',
          emailId: 'ch@example.com',
          country: 'AT',
        },
        mockData: null,
        expectedOutput: null,
        expectedError: new Error('Simulated BigQuery Error'),
      },
    ],
  };

  tests.success.forEach((t) => {
    it(t.name, async () => {
      mockBigQuery.data = t.mockData;

      const result = await query(t.input, mockBigQuery);
      assert.deepStrictEqual(result, t.expectedOutput);
    });
  });

  tests.error.forEach((t) => {
    it(t.name, async () => {
      mockBigQuery.error = t.expectedError;

      await assert.rejects(async () => {
        await query(t.input, mockBigQuery);
      }, {
        message: t.expectedError.message,
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Key element

mock.fn(): A powerful tool from node:test to create mock functions. We use it to stub out the createQueryStream method of the BigQuery client.

Mock implementation of createQueryStream simulates data streaming and errors through callbacks.

"Using a table-driven test format helps minimize code duplication and efficiently manage new test scenarios."

Running the Tests

Executing tests is as simple as running the following command in terminal:

node --test
Enter fullscreen mode Exit fullscreen mode

Node.js will automatically discover and execute test files withing project.
The output looks like:

node --test

Why Node.Js Native Test Runner?

Built-in Goodness: Embrace the simplicity of using Node.js's native testing capabilities.

Streamlined Workflow: No need for external dependencies, making your project setup cleaner.

Improved Readability: node:test encourages well-structured tests that are easy to understand and maintain.

Future-Proofing: Align your testing practices with the future of Node.js development.

Time to Test Smarter, Not Harder

This article aimed to introduce you to the Node.js native test runner and demonstrate how to write test functions with straightforward mocking, similar to other testing frameworks.

Give native testing a try in your next project and see how easy it is to write clean, efficient tests with built-in mocking. Your code will be more reliable and easier to maintain.

I'm passionate about learning, sharing and helping developers, so I've started a collection of Node.js native test runner examples at https://github.com/TheCodersStudio/node-native-test-runner.

Happy coding!

Top comments (0)