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
- Suite: A group of tests that are related. A suite can contain multiple test cases.
- Describe: This method is used to define a suite.
describe('Calculator Suite', function() {
// tests go here
});
-
Spec: A single test case, defined using the
itmethod.
it('should add numbers correctly', function() {
// test code
});
- 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);
});
});
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:
-
toBe(): Strict equality (===).
expect(5 + 5).toBe(10); // true
expect(5 + 5).toBe('10'); // false
-
toEqual(): Checks for deep equality, useful for objects.
expect({key: 'value'}).toEqual({key: 'value'}); // true
-
toBeTruthy()/toBeFalsy(): Checks if a value is truthy or falsy.
expect(5).toBeTruthy(); // true
expect(0).toBeFalsy(); // true
-
toBeNull(): Checks if the value isnull.
expect(calculator.total).toBeNull(); // true
-
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
-
toThrow(): Checks if a function throws an error.
expect(() => { calculator.divide(0); }).toThrow(); // true
-
toMatch(): Checks if a string matches a regular expression.
expect('my string').toMatch(/string$/); // true
Asymmetric Matchers
-
anything(): Matches any value exceptnullorundefined. -
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);
});
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
});
});
});
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);
});
});
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();
});
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!');
});
});
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,
});
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;
}
}
Key Points:
-
Getter Syntax: The
getfunction defines a computed property. - Configurable: Allows property redefinition or deletion.
-
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);
});
});
Explanation:
-
spyOn: Mock the
getElementByIdmethod to return a dummy object. -
spyOnProperty: Spy on the
versiongetter 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;
});
}
}
Jasmine Tests
- Testing Asynchronous Calls with
doneCallback
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
});
});
});
- 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();
});
});
});
- Here,
spyOn(window, 'fetch')intercepts thefetchmethod and provides a mock response. -
Promise.resolve()is used to simulate the API response with the version0.4.
- Using
async/awaitfor 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');
});
});
- The
awaitkeyword pauses the execution until the promise returned bycalculator.versionis resolved. - This approach eliminates the need for the
donecallback and makes the test more readable.
Summary of Testing Techniques
-
doneCallback:- The
donecallback is useful for handling asynchronous code. Jasmine will wait untildone()is called to consider the test complete. This is necessary for ensuring that asynchronous operations (like fetching data) have finished before making assertions.
- The
-
spyOnfor Mocking Network Requests:-
spyOnis used to mock the behavior of functions (likefetch). 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.
-
-
async/await:- Using
async/awaitmakes 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.
- Using
-
Mocking with
ResponseObject:- To simulate the
fetchAPI response, we usenew Response(), which creates a mockResponseobject. In this case, we return aversionfrom the mocked API.
- To simulate the
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
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
Configuration
Run the following command to create a Karma configuration file:
npx karma init
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
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;
}
}
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);
});
});
Folder Structure
project/
├── src/
│ ├── calculator.js
│ └── calculator.spec.js
├── karma.conf.js
├── package.json
└── node_modules/
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) usingit().
4. Asynchronous Testing:
- Jasmine handles asynchronous code using
done()callbacks, but modern JavaScript allows usingasync/awaitfor 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/awaitsyntax 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)