Jasmine and Karma

Unit testing JS Apps using Jasmine and Karma

Updated: 03 September 2023

Jasmine

What and Why

What

  • Javascript testing framework

    • Can be used with different languages and frameworks

      • Javascript, Node

      • Typescript, Angular

      • Ruby

      • Python

        Why

  • Fast

  • Independent

  • Runs via Node or in Browser

  • Behaviour Driven Development

    • Attempts to describe tests in a human readable format

Setting up a test environment

For JS

1
<!-- Order of tags is important -->
2
<link rel="stylesheet" href="jasmine.css" />
3
<script src="jasmine.js"></script>
4
<script src="jasmine-html.js"></script>
5
<script src="boot.js"></script>
6
7
<!-- Sctipts to be tested -->
8
<script src="main.js"></script>
9
10
<!-- Test Script -->
11
<script src="test.js"></script>

For Node

Add Jasmine as a dev dependency

1
npm install --save-dev jasmine

Add test command to package.json

1
"scripts": { "test": "jasmine" }

Run the test command

1
npm test

Writing a test suite and spec

We want to test the following helloWorld function.

main.js

1
function helloWorld() {
2
return 'Hello world!'
3
}

This function, when called should return 'Hello World'

test.js

1
describe('suiteName', () => {
2
it('specName', () => {
3
expect(helloWorld()).matcher('Hello world!')
4
})
5
})

4 functions

  • Define the Test Suite

    1
    describe(string, function)
  • Define a Test Spec

    1
    it(string, function)
  • Define the Actual Value

    1
    expect(actual)
  • Define the Comparison

    1
    matcher(expected)

Setup and Teardown

1
let expected = 'Hello World!'
  • Initialize variable that are needed for testing
1
beforeAll(function)
  • Called before all tests in a Suite are run
1
afterAll(function)
  • Called after all tests in a Suite are run
1
beforeEach(function)
  • Called before each test Spec is run
1
afterEach
  • Called after each test Spec is run

Matchers

  • The means of comparing the actual and expected values

  • Return a boolean indicating if a test passed or failed

  • Jasmine has some pre-built Matchers

    1
    expect(array).toContain(member)
    2
    expect(fn).toThrow(string)
    3
    expect(fn).toThrowError(string)
    4
    expect(instance).toBe(instance)
    5
    expect(mixed).toBeDefined()
    6
    expect(mixed).toBeFalsy()
    7
    expect(mixed).toBeNull()
    8
    expect(mixed).toBeTruthy()
    9
    expect(mixed).toBeUndefined()
    10
    expect(mixed).toEqual(mixed)
    11
    expect(mixed).toMatch(pattern)
    12
    expect(number).toBeCloseTo(number, decimalPlaces)
    13
    expect(number).toBeGreaterThan(number)
    14
    expect(number).toBeLessThan(number)
    15
    expect(number).toBeNaN()
    16
    expect(spy).toHaveBeenCalled()
    17
    expect(spy).toHaveBeenCalledTimes(number)
    18
    expect(spy).toHaveBeenCalledWith(...arguments)
  • Custom matchers can also be defined

    Running tests

  • For JS we have 2 options

  • Open index.html in a browser

  • Run npm test

Jasmine Demo

The following 3 files

index.html

1
<!DOCTYPE html>
2
<html>
3
<head>
4
<title>Jasmine Running</title>
5
<meta charset="UTF-8" />
6
<meta name="viewport" content="width=device-width, initial-scale=1" />
7
<link
8
rel="stylesheet"
9
href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css"
10
/>
11
</head>
12
<body>
13
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>
14
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>
15
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>
16
<script src="main.js"></script>
17
<script src="test.js"></script>
18
</body>
19
</html>

main.js

1
function helloWorld() {
2
return 'Hello world!'
3
}
4
5
function byeWorld() {
6
return 'Bye world!'
7
}
8
9
function bigNumber(num) {
10
return num + 1
11
}

test.js

1
describe('Test Suite', () => {
2
let hello = ''
3
let bye = 'Bye World :('
4
let num = 0
5
6
beforeAll(() => {
7
num = 5
8
})
9
10
beforeEach(() => {
11
hello = 'Hello world!'
12
})
13
14
afterEach(() => {
15
hello = ''
16
console.log('Test Completed')
17
})
18
19
it('Says hello', () => {
20
expect(helloWorld()).toEqual(hello)
21
})
22
23
it('Says bye', () => {
24
expect(byeWorld()).toEqual(bye)
25
})
26
27
it('Returns a bigger number', () => {
28
expect(bigNumber(num)).toBeGreaterThan(num)
29
})
30
})

Karma

What and Why

What

  • Test Runner for Jasmine in Angular

  • Automated running of tests

  • Does not require us to modify our code

  • Can test on multiple browser instances at once

    Testing Angular Components

    -ng generate component <component> will output:

    • component.html
    • component.css
    • component.ts
    • component.spec.ts
  • Test against an instance of a component

  • Using the Angular Test Bed

Angular Test Bed

  • Test behaviour that depends on Angular framework
  • Test Change and Property Binding
  • Import the TestBed, ComponentFixture and Component to be tested
1
import { TestBed, async } from '@angular/core/testing'
2
import { AppComponent } from './app.component'
  • Configure the Test Bed’s Testing Module with the necerssary components and imports in the beforeEach

  • Reinstantiate the component before each test

    1
    describe('AppComponent', () => {
    2
    beforeEach(async(() => {
    3
    TestBed.configureTestingModule({
    4
    declarations: [
    5
    AppComponent
    6
    ],
    7
    }).compileComponents();
    8
    }));
    9
    ...
    10
    });
  • Create a fixture and Component Instance

    • wrapper for a Component and Template

      1
      const fixture = TestBed.createComponent(AppComponent)
      2
      const app = fixture.debugElement.componentInstance
  • If a component has injected dependencies we can get these instances by:

    1
    const service = TestBed.get(ServiceName)

Looking at the App Component

app.component.html

1
<div style="text-align:center">
2
<h1>Welcome to {{ title }}!</h1>
3
</div>
4
<h2>Here are some links to help you start:</h2>
5
<ul>
6
<li>
7
<h2><a target="_blank" rel="noopener" href="...">Tour of Heroes</a></h2>
8
</li>
9
<li>
10
<h2><a target="_blank" rel="noopener" href="...">CLI Documentation</a></h2>
11
</li>
12
<li>
13
<h2><a target="_blank" rel="noopener" href="...">Angular blog</a></h2>
14
</li>
15
</ul>

app.component.ts

1
import { Component } from '@angular/core'
2
3
@Component({
4
selector: 'app-root',
5
templateUrl: './app.component.html',
6
styleUrls: ['./app.component.css'],
7
})
8
export class AppComponent {
9
title = 'app'
10
}

app.component.spec.ts

1
import { TestBed, async } from '@angular/core/testing'
2
import { AppComponent } from './app.component'
3
describe('AppComponent', () => {
4
beforeEach(async(() => {
5
TestBed.configureTestingModule({
6
declarations: [AppComponent],
7
}).compileComponents()
8
}))
9
it('should create the app', async(() => {
10
const fixture = TestBed.createComponent(AppComponent)
11
const app = fixture.debugElement.componentInstance
12
expect(app).toBeTruthy()
13
}))
14
it(`should have as title 'app'`, async(() => {
15
const fixture = TestBed.createComponent(AppComponent)
16
const app = fixture.debugElement.componentInstance
17
expect(app.title).toEqual('app')
18
}))
19
it('should render title in a h1 tag', async(() => {
20
const fixture = TestBed.createComponent(AppComponent)
21
fixture.detectChanges()
22
const compiled = fixture.debugElement.nativeElement
23
expect(compiled.querySelector('h1').textContent).toContain(
24
'Welcome to app!'
25
)
26
}))
27
})

Running tests with the AngularCLI

  • ng test
  • Other Angular features can also be tested
    • Services
    • Components
    • Classes
    • Forms
    • Routing
    • Dependency Injection
    • etc.

Karma Demo

Conclusion

  • Jasmine is a relatively simple testing tool
    • Easy to implement on a variety of projects
  • Karma Automates testing
    • Test on multiple places simultaneously
    • Well incorporated into Angular