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

Chetan PatilChetan Patil
5 min read

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,
};

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();

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,
      });
    });
  });
});

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

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

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 DevInsightLab/node-native-test-runner.

Happy coding!

0
Subscribe to my newsletter

Read articles from Chetan Patil directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chetan Patil
Chetan Patil

I'm Chetan Patil, a full-stack software engineer and cloud enthusiast with a passion for building impactful and innovative solutions. I thrive on the power of technology to shape a better world, and I channel that energy into every project I tackle. Currently, I'm putting my skills to work as a Senior Software Engineer at IKEA - Ingka Group in Sweden. Here, I leverage cutting-edge technologies like JavaScript, TypeScript, Node.js, and Golang to create robust software solutions that enhance the IKEA experience. My journey began at Codesphere Solutions, where I honed my craft and developed a passion for delivering high-quality software. I'm a firm believer in continuous learning and constantly seek out new technologies and challenges to expand my skillset. Sharing my knowledge and experiences is important to me, which is why I actively contribute to the developer community through my technical articles on Medium and npm packages. Let's connect! I'm always excited to engage with fellow developers, exchange ideas, and explore potential collaborations. You can find me on LinkedIn and npm – let's build something amazing together!