DEV Community

Renuka Patil
Renuka Patil

Posted on

Jasmin Testing Framework: A Comprehensive Guide

Unit Testing: This refers to testing the smallest unit of code, typically a single function or component. The goal is to ensure that the function works as expected in isolation, without depending on other parts of the application.

Key Terms

  1. Suite: A group of tests that are related. A suite can contain multiple test cases.
  2. Describe: This method is used to define a suite.
   describe('Calculator Suite', function() {
       // tests go here
   });
Enter fullscreen mode Exit fullscreen mode
  1. Spec: A single test case, defined using the it method.
   it('should add numbers correctly', function() {
       // test code
   });
Enter fullscreen mode Exit fullscreen mode
  1. Expectation: A statement that checks if something is true or false.

Example: expect(5 + 5).toBe(10); checks if the sum of 5 + 5 equals 10.

Example: Basic Calculator Test Suite

describe('Calculator', function() {

   it('should add numbers correctly', function() {
       const calculator = new Calculator();
       calculator.add(5);
       expect(calculator.total).toBe(5);
   });

   it('should subtract numbers correctly', function() {
       const calculator = new Calculator();
       calculator.total = 30;
       calculator.subtract(5);
       expect(calculator.total).toBe(25);
   });

   it('should multiply numbers correctly', function() {
       const calculator = new Calculator();
       calculator.total = 30;
       calculator.multiply(5);
       expect(calculator.total).toBe(150);
   });

   it('should divide numbers correctly', function() {
       const calculator = new Calculator();
       calculator.total = 30;
       calculator.divide(5);
       expect(calculator.total).toBe(6);
   });

});
Enter fullscreen mode Exit fullscreen mode

Disabling Tests

You can disable a test or suite using x:

  • xit(): Marks a spec as pending.
  • xdescribe(): Marks a suite as pending.

Matchers in Jasmine

Matchers are used to compare actual values to expected ones. Here are some of the most common ones:

  1. toBe(): Strict equality (===).
   expect(5 + 5).toBe(10); // true
   expect(5 + 5).toBe('10'); // false
Enter fullscreen mode Exit fullscreen mode
  1. toEqual(): Checks for deep equality, useful for objects.
   expect({key: 'value'}).toEqual({key: 'value'}); // true
Enter fullscreen mode Exit fullscreen mode
  1. toBeTruthy() / toBeFalsy(): Checks if a value is truthy or falsy.
   expect(5).toBeTruthy(); // true
   expect(0).toBeFalsy();  // true
Enter fullscreen mode Exit fullscreen mode
  1. toBeNull(): Checks if the value is null.
   expect(calculator.total).toBeNull(); // true
Enter fullscreen mode Exit fullscreen mode
  1. toContain(): Checks if an array contains an element or if a string contains a substring.
   expect([1, 2, 3]).toContain(2); // true
   expect('hello world').toContain('hello'); // true
Enter fullscreen mode Exit fullscreen mode
  1. toThrow(): Checks if a function throws an error.
   expect(() => { calculator.divide(0); }).toThrow(); // true
Enter fullscreen mode Exit fullscreen mode
  1. toMatch(): Checks if a string matches a regular expression.
   expect('my string').toMatch(/string$/); // true
Enter fullscreen mode Exit fullscreen mode

Asymmetric Matchers

  • anything(): Matches any value except null or undefined.
  • any(): Matches any instance of a class or constructor.
  • ObjectContaining(): Matches an object containing the specified properties.
  • StringContaining(): Matches a string containing the specified substring.

Custom Matchers

Custom matchers allow you to define your own comparison logic. Here's an example of a custom matcher for Calculator:

const customMatcher = {
   toBeCalculator: function() {
      return {
         compare: function(actual, expected) {
            const result = {};
            if (actual instanceof Calculator) {
               result.pass = true;
               result.message = 'The value is a Calculator';
            } else {
               result.pass = false;
               result.message = 'The value is not a Calculator';
            }
            return result;
         }
      };
   }
};

beforeEach(function() {
   jasmine.addMatchers(customMatcher);
});
Enter fullscreen mode Exit fullscreen mode

Nested Suites

Jasmine allows you to nest describe blocks to organize your tests.

describe('Calculator', function() {
   describe('Addition', function() {
      it('should add numbers correctly', function() {
         // test code
      });
   });
   describe('Subtraction', function() {
      it('should subtract numbers correctly', function() {
         // test code
      });
   });
});
Enter fullscreen mode Exit fullscreen mode

Setup and Teardown

  • beforeEach(): Runs before each spec.
  • beforeAll(): Runs once before the suite.
  • afterEach(): Runs after each spec.
  • afterAll(): Runs once after the suite.

Example of using beforeEach and afterEach:

describe('Calculator Suite', function() {
   let calculator;

   beforeEach(function() {
      calculator = new Calculator();
   });

   afterEach(function() {
      // cleanup if needed
   });

   it('should add numbers correctly', function() {
      calculator.add(5);
      expect(calculator.total).toBe(5);
   });
});
Enter fullscreen mode Exit fullscreen mode

Spies

Spies are used to track function calls, arguments, and return values. They allow you to isolate dependencies and mock external calls during testing.

spyOn(): Creates a spy for an existing function.
toHaveBeenCalled(): Ensures a function has been called.
toHaveBeenCalledWith(): Checks if a function was called with specific arguments.
toHaveBeenCalledTimes(): Checks how many times a function was called.
Example:

it('should validate expression if the number is invalid', function() {
  spyOn(window, 'updateResult').and.stub();
  calculate('a+3');
  expect(window.updateResult).toHaveBeenCalled();
});
Enter fullscreen mode Exit fullscreen mode

The this Keyword:

You can use this inside the beforeAll, afterAll, and it() blocks to refer to the current test context (e.g., elements created in beforeAll).

Example:

describe('DOM manipulation', function() {
  beforeAll(function() {
    this.element = document.createElement('div');
    document.body.appendChild(this.element);
  });

  afterAll(function() {
    document.body.removeChild(this.element);
  });

  it('should update the DOM', function() {
    this.element.innerText = 'Hello, Jasmine!';
    expect(this.element.innerText).toBe('Hello, Jasmine!');
  });
});
Enter fullscreen mode Exit fullscreen mode

Understanding JavaScript Getter Properties and Automating Tests with Karma

Getter properties allow us to define a function in an object or prototype that acts like a property. This is useful for computed properties or encapsulating property access logic. Here's how you can define a getter using Object.defineProperty:

Object.defineProperty(Calculator.prototype, 'version', {
  get: function () {
    return '0.1';
  },
  configurable: true,
  enumerable: true,
});
Enter fullscreen mode Exit fullscreen mode

Example Usage

We can now access the version property of the Calculator object as if it were a regular property:

function showVersion() {
  const calculator = new Calculator();
  const element = document.getElementById('version');
  if (element) {
    element.innerText = calculator.version;
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  1. Getter Syntax: The get function defines a computed property.
  2. Configurable: Allows property redefinition or deletion.
  3. Enumerable: Makes the property appear in object enumerations like for...in.

Writing Tests for the Getter Property

We will write specs for the showVersion function using Jasmine, a behavior-driven development framework for JavaScript.

describe('showVersion()', function () {
  it('should call the version getter', function () {
    spyOn(document, 'getElementById').and.returnValue({
      innerText: null,
    });

    const spy = spyOnProperty(Calculator.prototype, 'version', 'get');
    showVersion();

    expect(spy).toHaveBeenCalled();
    expect(spy).toHaveBeenCalledTimes(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • spyOn: Mock the getElementById method to return a dummy object.
  • spyOnProperty: Spy on the version getter to verify that it was called.
  • Assertions: Check that the getter is invoked and count its calls.

Testing Asynchronous Fetch Calls in Jasmine: Using Async/Await and SpyOn

Handling asynchronous API calls in Jasmine tests, along with the use of async/await, done callbacks, and spyOn to mock the API response.

Code Implementation for Fetching Version

The code you've provided adds an asynchronous getter to the Calculator class, fetching the version from an external API.

Object.defineProperty(Calculator.prototype, 'version', {
  get: function() {
    return fetch('api url here')
      .then(function(result) {
        return result.json();
      })
      .then(function(jsonData) {
        return jsonData.version;
      });
  },
  configurable: true,
  enumerable: true,
});

function showVersion() {
  const calculator = new Calculator();
  const element = document.getElementById('version');
  if (element) {
    calculator.version.then(function(version) {
      element.innerText = version;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Jasmine Tests

  1. Testing Asynchronous Calls with done Callback

The done callback in Jasmine is used to let Jasmine know when an asynchronous operation has completed. In the test, we call done() after checking the version.

   describe('get Version', function() {
     it('fetches version from external source', function(done) {
       calculator.version.then(function(version) {
         expect(version).toBe('0.1');
         done(); // Call done when the asynchronous operation is complete
       });
     });
   });
Enter fullscreen mode Exit fullscreen mode
  1. Avoiding Real API Calls Using spyOn

Since we don't want to make real network requests during tests, we use Jasmine’s spyOn to mock the fetch call. We use and.returnValue() to return a resolved Promise that mimics a successful API response.

   describe('get Version', function() {
     it('fetches version from external source', function(done) {
       spyOn(window, 'fetch').and.returnValue(Promise.resolve(new Response(JSON.stringify({ version: '0.4' }))));

       calculator.version.then(function(version) {
         expect(version).toBe('0.4');
         done();
       });
     });
   });
Enter fullscreen mode Exit fullscreen mode
  • Here, spyOn(window, 'fetch') intercepts the fetch method and provides a mock response.
  • Promise.resolve() is used to simulate the API response with the version 0.4.
  1. Using async/await for Asynchronous Code

In the async/await approach, we mark the test function as async and use await to wait for the promise to resolve.

   describe('get Version', function() {
     it('fetches version from external source', async function() {
       spyOn(window, 'fetch').and.returnValue(Promise.resolve(new Response(JSON.stringify({ version: '0.4' }))));

       const version = await calculator.version;
       expect(version).toBe('0.4');
     });
   });
Enter fullscreen mode Exit fullscreen mode
  • The await keyword pauses the execution until the promise returned by calculator.version is resolved.
  • This approach eliminates the need for the done callback and makes the test more readable.

Summary of Testing Techniques

  1. done Callback:

    • The done callback is useful for handling asynchronous code. Jasmine will wait until done() is called to consider the test complete. This is necessary for ensuring that asynchronous operations (like fetching data) have finished before making assertions.
  2. spyOn for Mocking Network Requests:

    • spyOn is used to mock the behavior of functions (like fetch). In tests, it prevents real network requests, ensuring that you can simulate API responses and test your code in isolation. It’s a key technique for avoiding network calls in unit tests.
  3. async/await:

    • Using async/await makes asynchronous code easier to read and manage. It simplifies handling promises, making your tests look more synchronous while keeping the non-blocking behavior of JavaScript.
  4. Mocking with Response Object:

    • To simulate the fetch API response, we use new Response(), which creates a mock Response object. In this case, we return a version from the mocked API.

These techniques allow for effective testing of asynchronous functions and external API calls, ensuring that tests remain fast and isolated from real-world data.


Initialize the NPM Package and Create package.json

To set up a JavaScript project, initialize the npm package:

npm init
Enter fullscreen mode Exit fullscreen mode

This command will prompt you for information and create a package.json file, which manages dependencies and scripts for your project.


Karma: The Test Runner for Automation

Karma is a test runner that allows you to automate tests in multiple browsers.

Installation

Install Karma and its required plugins:

npm install karma --save-dev
npm install jasmine-core karma-jasmine --save-dev
npm install karma-chrome-launcher --save-dev
Enter fullscreen mode Exit fullscreen mode

Configuration

Run the following command to create a Karma configuration file:

npx karma init
Enter fullscreen mode Exit fullscreen mode

Choose the testing framework (Jasmine), browsers (e.g., Chrome), and other options during the setup process.

Running Tests

Once configured, you can run tests using:

npx karma start
Enter fullscreen mode Exit fullscreen mode

Final Code and Folder Structure

File: calculator.js

function Calculator() {}

Object.defineProperty(Calculator.prototype, 'version', {
  get: function () {
    return '0.1';
  },
  configurable: true,
  enumerable: true,
});

function showVersion() {
  const calculator = new Calculator();
  const element = document.getElementById('version');
  if (element) {
    element.innerText = calculator.version;
  }
}
Enter fullscreen mode Exit fullscreen mode

File: calculator.spec.js

describe('showVersion()', function () {
  it('should call the version getter', function () {
    spyOn(document, 'getElementById').and.returnValue({
      innerText: null,
    });

    const spy = spyOnProperty(Calculator.prototype, 'version', 'get');
    showVersion();

    expect(spy).toHaveBeenCalled();
    expect(spy).toHaveBeenCalledTimes(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

Folder Structure

project/
├── src/
│   ├── calculator.js
│   └── calculator.spec.js
├── karma.conf.js
├── package.json
└── node_modules/
Enter fullscreen mode Exit fullscreen mode

Summary

Here’s a comprehensive summary of the blog :

1. Introduction to Jasmine:

  • Jasmine is a popular JavaScript testing framework designed for behavior-driven development (BDD). It allows developers to write tests in a clear, readable format.
  • It's widely used for unit testing JavaScript code and provides utilities like spies, matchers, and asynchronous testing tools.

2. Setting up Jasmine:

  • Jasmine can be easily set up by including its script in your project. Alternatively, you can use Node.js and NPM to install it as a dependency.
  • Jasmine is commonly used with tools like Karma for running tests in various browsers, or directly in the command line for Node.js-based projects.

3. Core Features of Jasmine:

  • Spies: Spies allow you to monitor function calls and arguments. You can use them to mock functions during testing and track how they're used.
  • Matchers: Jasmine offers a set of built-in matchers to assert conditions like toBe(), toEqual(), toContain(), etc. Matchers compare the actual and expected values in tests.
  • Suites and Specs: Jasmine organizes tests into Suites (test groups) using describe() and Specs (individual tests) using it().

4. Asynchronous Testing:

  • Jasmine handles asynchronous code using done() callbacks, but modern JavaScript allows using async/await for more manageable code.
  • This section discusses testing asynchronous behavior, such as waiting for promises to resolve or handling APIs.

5. Mocking with Spies:

  • You can use spyOn() to create spies that replace real function implementations with mock versions. This allows you to track function calls without triggering real behavior, such as network requests.

6. Practical Testing Examples:

  • Mocking Network Requests: Example of testing a function that fetches data from an API, using spyOn() to avoid making real API calls during tests.
  • Testing Async Functions: Practical examples show how Jasmine can be used to test functions that return promises, like those fetching data or performing asynchronous calculations.

7. Testing Async with async/await:

  • Example tests show how Jasmine supports the async/await syntax to handle asynchronous code, making the tests easier to write and read.

8. Using Jasmine with Karma:

  • Karma is introduced as a tool for running Jasmine tests in multiple browsers. It helps ensure that the code behaves consistently across different environments.

9. Mocking and Spying Best Practices:

  • Best practices are discussed for using spies in a way that avoids overcomplicating tests. For instance, spies can replace specific methods or entire objects to isolate the tests from external dependencies.

10. Integration with Other Tools:

  • Jasmine integrates well with other testing tools like Karma for continuous integration and Sinon for advanced spying and stubbing.

11. Conclusion:

  • Jasmine is an easy-to-use, robust testing framework that works well for testing JavaScript in both browser and Node environments.
  • With features like spies, matchers, and the ability to handle asynchronous testing, Jasmine is an ideal choice for developers aiming to write reliable, maintainable unit tests.

This guide equips developers with the knowledge to effectively use Jasmine in their projects, from setting it up to mastering advanced concepts like spies and asynchronous testing.

Stay tuned for more JavaScript tips and tricks!

Top comments (0)