Jasmine and Karma

Unit testing JS Apps using Jasmine and Karma

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

<!-- Order of tags is important -->
<link rel="stylesheet" href="jasmine.css" />
<script src="jasmine.js"></script>
<script src="jasmine-html.js"></script>
<script src="boot.js"></script>

<!-- Sctipts to be tested -->
<script src="main.js"></script>

<!-- Test Script -->
<script src="test.js"></script>

For Node

Add Jasmine as a dev dependency

npm install --save-dev jasmine

Add test command to package.json

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

Run the test command

npm test

Writing a test suite and spec

We want to test the following helloWorld function.

main.js

function helloWorld() {
  return "Hello world!"
}

This function, when called should return 'Hello World'

test.js

describe("suiteName", () => {
  it("specName", () => {
    expect(helloWorld()).matcher("Hello world!")
  })
})

4 functions

  • Define the Test Suite
  describe(string, function)
  • Define a Test Spec
  it(string, function)
  • Define the Actual Value
  expect(actual)
  • Define the Comparison
  matcher(expected)

Setup and Teardown

let expected = "Hello World!"
  • Initialize variable that are needed for testing
beforeAll(function)
  • Called before all tests in a Suite are run
afterAll(function)
  • Called after all tests in a Suite are run
beforeEach(function)
  • Called before each test Spec is run
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
  expect(array).toContain(member)
  expect(fn).toThrow(string)
  expect(fn).toThrowError(string)
  expect(instance).toBe(instance)
  expect(mixed).toBeDefined()
  expect(mixed).toBeFalsy()
  expect(mixed).toBeNull()
  expect(mixed).toBeTruthy()
  expect(mixed).toBeUndefined()
  expect(mixed).toEqual(mixed)
  expect(mixed).toMatch(pattern)
  expect(number).toBeCloseTo(number, decimalPlaces)
  expect(number).toBeGreaterThan(number)
  expect(number).toBeLessThan(number)
  expect(number).toBeNaN()
  expect(spy).toHaveBeenCalled()
  expect(spy).toHaveBeenCalledTimes(number)
  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

<!DOCTYPE html>
<html>
  <head>
    <title>Jasmine Running</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css"
    />
  </head>
  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>
    <script src="main.js"></script>
    <script src="test.js"></script>
  </body>
</html>

main.js

function helloWorld() {
  return "Hello world!"
}

function byeWorld() {
  return "Bye world!"
}

function bigNumber(num) {
  return num + 1
}

test.js

describe("Test Suite", () => {
  let hello = ""
  let bye = "Bye World :("
  let num = 0

  beforeAll(() => {
    num = 5
  })

  beforeEach(() => {
    hello = "Hello world!"
  })

  afterEach(() => {
    hello = ""
    console.log("Test Completed")
  })

  it("Says hello", () => {
    expect(helloWorld()).toEqual(hello)
  })

  it("Says bye", () => {
    expect(byeWorld()).toEqual(bye)
  })

  it("Returns a bigger number", () => {
    expect(bigNumber(num)).toBeGreaterThan(num)
  })
})

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
import { TestBed, async } from "@angular/core/testing"
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
  describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));
  ...
  });
  • Create a fixture and Component Instance

    • wrapper for a Component and Template
    const fixture = TestBed.createComponent(AppComponent)
    const app = fixture.debugElement.componentInstance
    
  • If a component has injected dependencies we can get these instances by:

  const service = TestBed.get(ServiceName)

Looking at the App Component

app.component.html

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<h2>Here are some links to help you start:</h2>
<ul>
  <li>
    <h2><a target="_blank" rel="noopener" href="...">Tour of Heroes</a></h2>
  </li>
  <li>
    <h2><a target="_blank" rel="noopener" href="...">CLI Documentation</a></h2>
  </li>
  <li>
    <h2><a target="_blank" rel="noopener" href="...">Angular blog</a></h2>
  </li>
</ul>

app.component.ts

import { Component } from "@angular/core"

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  title = "app"
}

app.component.spec.ts

import { TestBed, async } from "@angular/core/testing"
import { AppComponent } from "./app.component"
describe("AppComponent", () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
    }).compileComponents()
  }))
  it("should create the app", async(() => {
    const fixture = TestBed.createComponent(AppComponent)
    const app = fixture.debugElement.componentInstance
    expect(app).toBeTruthy()
  }))
  it(`should have as title 'app'`, async(() => {
    const fixture = TestBed.createComponent(AppComponent)
    const app = fixture.debugElement.componentInstance
    expect(app.title).toEqual("app")
  }))
  it("should render title in a h1 tag", async(() => {
    const fixture = TestBed.createComponent(AppComponent)
    fixture.detectChanges()
    const compiled = fixture.debugElement.nativeElement
    expect(compiled.querySelector("h1").textContent).toContain(
      "Welcome to app!"
    )
  }))
})

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