Javascript is required
·
11 min read
·
4366 views

Property Based Testing With Typescript

Property Based Testing With Typescript Image

In my current project my colleague Michael Seifert introduced property based testing in our Python codebase. It was the first time I heard about it and it sounded fascinating, so I wanted to also implement it in our frontend code based on Vue.js with Jest as testing framework and TypeScript as programming language.

In this article I want to give you an introduction to property based testing and show you how you can use it in the most used TypeScript-based testing frameworks like Jest, Karma, and Mocha.

Example Based Testing

TL;DR: A single concrete example of expected behavior of the system under test.

Let me first describe how most of us developers usually write their unit tests.

Let's assume we want to test this simple TypeScript function:

1/**
2 * Returns the position of the first occurrence of `pattern` in `text`
3 */
4export const indexOf = (text: string, pattern: string): number => {
5  return text.indexOf(pattern)
6}

Typical unit tests for this method using Jest or Mocha would be:

1describe('Example based tests', () => {
2  it('should return -1 if text does not contain the given pattern', () => {
3    expect(indexOf('abc123', 'zzz')).toBe(-1)
4  })
5
6  it('should return 0 if text contains the given pattern', () => {
7    expect(indexOf('123abc', '123')).toBe(0)
8  })
9
10  it('should return 0 if empty strings are compared', () => {
11    expect(indexOf('', '')).toBe(0)
12  })
13})

So basically we define a set of certain inputs, and the expected result of our function under test if it executes with this given input. If the set of examples is well-chosen the tests can provide high confidence that the function behaves as expected.

As you can imagine, there can be many permutations and mutations of possible inputs and that's exactly the use case where property based testing might be useful for your application.

What is Property Based Testing?

TL;DR: Another way to test programs with generated random (but constrained) inputs instead of relying on hard-coded inputs and outputs.

Property based testing has been introduced by the QuickCheck framework in Haskell and since then it has become quite famous especially in functional programming.

It provides another approach to example based testing and can cover tests as unit, integration and even E2E (end-to-end) tests (which I will cover later in this article).

As the name suggests, property based testing relies on properties. You can think of a property as a trait you expect to see in your output by your given inputs. The expected result does not have to be itself and most of the time it will not be.

An exemplary property :

for all (x, y, ...)

such as precondition(x, y, ...) holds

property(x, y, ...) is true

Using properties, we could state that:

for any strings a, b and c

b is a substring of a + b + c

The testing framework will take this information, generate multiple random entries and runs checks on them. If the test fails, it will provide the used seed and a counterexample. The suggested counterexample is the minimal failing counterexample.

For this substring example: whenever the tested string contains a . in itself, the above check fails and the minimal counterexample would be {a: '.', b: '', c: ''} and not something like {a: 'y837dD!d.', b: 'acxSAD4', c: '!y,wqe2"'}.

As a result, our code is tested more thoroughly and we might find unexpected bugs while running our tests.

Benefits

  • Coverage: Theoretically, all possible inputs are generated without any restrictions which can cover the whole range of integers, string or whatever type you need for your test. This can help to discover unexplored code paths in your program.
  • Reproducible: A seed is produced each time a property test runs. Using this seed it is possible to re-run a test with the same set of data. If the test run fails, the seed and the failing test will be printed to the command line so that it is fully reproducible.
  • Shrink: After a failing test, the framework tries to reduce the input to a smaller input. An example: If your test fails due to a certain character in a string the framework will run the test again with a string that only contains this certain character.

It is also important to note that it does not — by any means — replace unit testing. It only provides an additional layer of tests that might prove very efficient to reduce some boilerplate tests.

Property based testing with TypeScript

Available Libraries

There exist two popular libraries for property based testing with TypeScript (and JavaScript): JSVerify and fast-check

I prefer fast-check for the following reasons:

  • It is more actively maintained.
  • It has strong and up-to-date built-in types thanks to TypeScript (the library itself is also written in TypeScript).

Writing a first fast-check test

To install fast-check you need to run this command in your terminal:

bash
npm i fast-check -D

Then you are already ready to use the library in your existing test framework, like in Jest or Mocha as shown in the following example:

1import * as fc from 'fast-check'
2
3describe('Property based tests', () => {
4  it('should always contain itself', () => {
5    fc.assert(fc.property(fc.string(), (text) => indexOf(text, text) !== -1))
6  })
7
8  it('should always contain its substrings', () => {
9    fc.assert(
10      fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {
11        // Alternatively: no return statement and direct usage of expect or assert
12        return indexOf(b, a + b + c) !== -1
13      })
14    )
15  })
16})

Let's take a quick look at the anatomy of our fast-check tests:

  • fc.assert runs the property
  • fc.property defines the property
  • fc.string() defines the inputs the framework has to generate
  • text => { ... } checks the output against the generated value

If we run this tests, we can see that we receive an error:

1Error: Property failed after 1 tests
2{ seed: -481667763, path: "0:0:0:1", endOnFailure: true }
3Counterexample: ["",""," "]
4Shrunk 3 time(s)
5Got error: Property failed by returning false

The error message is correct, and we found an edge-case for our indexOf method under test which we most probably would not have discovered with example based testing.

With these simple steps you can easily introduce property based testing to projects which use Jest or Mocha as test framework independent of the web framework you are using. The code for this demo is available at GitHub.

Angular & Karma Demo

In the following demo, I want to show you how you can integrate property based testing in an Angular application (which per default uses Karma) as test runner. Additionally, I also want to demonstrate the usage of property based testing for end-to-end (E2E) tests using Protractor. The code for this demos is available at GitHub.

First Karma property based unit test

As a base we use an Angular project created with the Angular CLI.

Next step is to install fast-check we, therefore, need to run this command in the terminal:

bash
npm i fast-check -D

For a first test, we add our indexOf test method to app.component.ts:

1@Component({
2  selector: 'app-root',
3  templateUrl: './app.component.html',
4  styleUrls: ['./app.component.sass'],
5})
6export class AppComponent {
7  title = 'angular-demo'
8
9  /**
10   * Returns the position of the first occurrence of `pattern` in `text`
11   */
12  indexOf(text: string, pattern: string): number {
13    return text.indexOf(pattern)
14  }
15}

Now we can modify the CLI-generated test app.component.spec.ts and add property based tests as we did it for the Typescript-Jest-Mocha demo before:

1import * as fc from 'fast-check'
2
3describe('AppComponent', () => {
4  beforeEach(async(() => {
5    TestBed.configureTestingModule({
6      declarations: [AppComponent],
7    }).compileComponents()
8  }))
9
10  describe('indexOf Property based tests', () => {
11    it('should always contain itself', () => {
12      const fixture = TestBed.createComponent(AppComponent)
13      const app = fixture.componentInstance
14      fc.assert(fc.property(fc.string(), (text) => app.indexOf(text, text) !== -1))
15    })
16
17    it('should always contain its substrings', () => {
18      const fixture = TestBed.createComponent(AppComponent)
19      const app = fixture.componentInstance
20      fc.assert(
21        fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {
22          // Alternatively: no return statement and direct usage of expect or assert
23          return app.indexOf(b, a + b + c) !== -1
24        })
25      )
26    })
27  })
28})

If we now run the tests, we get the same result:

1Error: Property failed after 1 tests
2    { seed: -1006000007, path: "0:0:1:0:0:0", endOnFailure: true }
3    Counterexample: ["",""," "]
4    Shrunk 5 time(s)
5    Got error: Property failed by returning false

More realistic example

Since now we just used very simple data for our tests but the reality is usually way more complex and we need to work with more complex data structures. For this purpose, a new service needs to be created using the Angular CLI via ng generate service user which simulates a more realistic scenario:

user.service.ts
1export interface Adress {
2  street: string
3  postalCode: number
4  city: string
5}
6
7export interface User {
8  name: string
9  age: number
10  addresses: Adress[]
11}
12
13@Injectable({
14  providedIn: 'root',
15})
16export class UserService {
17  isValidUser(user: User): boolean {
18    const { name, age, addresses } = user
19
20    if (!name.trim()) {
21      console.error('Name must be defined')
22      return false
23    }
24
25    if (age < 0 || age > 150) {
26      console.error('Age must be greater than 0 and below 150')
27      return false
28    }
29
30    for (const address of addresses) {
31      const { street, postalCode, city } = address
32      if (!street.trim()) {
33        console.error('Address must contain a street')
34        return false
35      }
36
37      if (postalCode === undefined) {
38        console.error('Address must contain a postal code')
39        return false
40      }
41
42      if (!city.trim()) {
43        console.error('Address must contain a city')
44        return false
45      }
46    }
47  }
48}

This demo service simulates a User object validation and its isValidUser method should be tested:

user.service.spec.ts
1import { TestBed } from '@angular/core/testing'
2
3import { UserService } from './user.service'
4import * as fc from 'fast-check'
5
6describe('UserService', () => {
7  let service: UserService
8
9  beforeEach(() => {
10    TestBed.configureTestingModule({})
11    service = TestBed.inject(UserService)
12  })
13
14  it('should be created', () => {
15    expect(service).toBeTruthy()
16  })
17
18  describe('isValidUser property based tests', () => {
19    it('should be valid user', () => {
20      const UserArbitrary = fc.record({
21        name: fc.string(6, 1000),
22        age: fc.integer(),
23        addresses: fc.array(
24          fc.record({
25            street: fc.string(6, 500),
26            postalCode: fc.integer(),
27            city: fc.string(6, 500),
28          })
29        ),
30      })
31
32      fc.assert(
33        fc.property(UserArbitrary, (user) => {
34          return service.isValidUser(user)
35        }),
36        { verbose: true } // have the list of all failing values encountered during the run
37      )
38    })
39  })
40})

The test looks similar to the our first TypeScript test but we now have a more complex JavaScript object which we want to generate using fc.record:

1const UserArbitrary = fc.record({
2  name: fc.string(6, 1000),
3  age: fc.integer(),
4  addresses: fc.array(
5    fc.record({
6      street: fc.string(6, 500),
7      postalCode: fc.integer(),
8      city: fc.string(6, 500),
9    })
10  ),
11})

Running the tests leads to a failed test run:

1Error: Property failed after 1 tests
2    { seed: -91394804, path: "0:0:0:1:0:0:0:0:0", endOnFailure: true }
3    Counterexample: [{"name":" 0!f>A","age":-1,"addresses":[]}]
4    Shrunk 8 time(s)
5    Got error: Property failed by returning false

According to our isValidUser method, a user cannot have an age smaller 1 or greater 150, so we need to adjust our record:

1const UserArbitrary = fc.record({
2  name: fc.string(6, 1000),
3  age: fc.integer(1, 150), // now it is valid
4  addresses: fc.array(
5    fc.record({
6      street: fc.string(6, 500),
7      postalCode: fc.integer(),
8      city: fc.string(6, 500),
9    })
10  ),
11})

As demonstrated, using property based testing in Angular applications is also very easy.

E2E test with Protractor

Another interesting use case of property based testing can be seen in end-to-end (E2E) test which I want to demonstrate using Protractor.

For this purpose I modified the HTML to have a simple form with two inputs and a submit button:

app.component.html
1<h1>Property Based Testing Protractor Demo</h1>
2
3<div class="container">
4  <h2>Demo Form</h2>
5  <p id="submitted-object">Submitted object: {{ submitted | json }}</p>
6  <form #demoForm="ngForm" (ngSubmit)="onSubmit()">
7    <div class="form-group">
8      <label for="demo-name-input">Name</label>
9      <input type="text" [(ngModel)]="anyName" name="demo-name" class="form-control" id="demo-name-input" required />
10    </div>
11
12    <div class="form-group">
13      <label for="demo-description-input">Description</label>
14      <input
15        type="text"
16        [(ngModel)]="description"
17        name="demo-description"
18        class="form-control"
19        id="demo-description-input"
20      />
21    </div>
22
23    <button type="submit" class="btn btn-success" id="demo-submit-button">Submit</button>
24  </form>
25</div>

The corresponding TypeScript code:

app.component.ts
1@Component({
2  selector: 'app-root',
3  templateUrl: './app.component.html',
4  styleUrls: ['./app.component.sass'],
5})
6export class AppComponent {
7  title = 'angular-demo'
8  anyName = 'A user'
9  description = ''
10  submitted?: { name: string; description: string }
11
12  /**
13   * Returns the position of the first occurrence of `pattern` in `text`
14   */
15  indexOf(text: string, pattern: string): number {
16    return text.indexOf(pattern)
17  }
18
19  onSubmit() {
20    this.submitted = { name: this.anyName, description: this.description }
21  }
22}

Based on this template I modified the page object to be able to interact with this page in a clean way:

app.po.ts
1import { browser, by, element } from 'protractor'
2
3export class AppPage {
4  navigateTo(): Promise<unknown> {
5    return browser.get(browser.baseUrl) as Promise<unknown>
6  }
7
8  getSubmittedText(): Promise<string> {
9    return element(by.id('submitted-object')).getText() as Promise<string>
10  }
11
12  enterName(name: string): Promise<void> {
13    const nameInput = element(by.id('demo-name-input'))
14    return nameInput.sendKeys(name) as Promise<void>
15  }
16
17  enterDescription(name: string): Promise<void> {
18    const descriptionInput = element(by.id('demo-description-input'))
19    return descriptionInput.sendKeys(name) as Promise<void>
20  }
21
22  submit(): Promise<void> {
23    const submitButton = element(by.id('demo-submit-button'))
24    return submitButton.click() as Promise<void>
25  }
26
27  clear() {
28    this.enterDescription('')
29    return this.enterName('')
30  }
31}

The final step is to write the actual E2E test:

app.e2e-spec.ts
1import { AppPage } from './app.po'
2import { browser, logging } from 'protractor'
3
4import * as fc from 'fast-check'
5
6describe('workspace-project App', () => {
7  let page: AppPage
8
9  beforeEach(() => {
10    page = new AppPage()
11  })
12
13  it('should correctly submit', () => {
14    page.navigateTo()
15
16    fc.assert(
17      fc.property(fc.string(), fc.lorem(), (name, description) => {
18        page.enterName(name)
19        page.enterDescription(description)
20        page.submit()
21        expect(page.getSubmittedText()).toBe(`Submitted object: ${JSON.stringify({ name, description })}`)
22        page.navigateTo()
23      })
24    )
25  })
26
27  afterEach(async () => {
28    // Assert that there are no errors emitted from the browser
29    const logs = await browser.manage().logs().get(logging.Type.BROWSER)
30    expect(logs).not.toContain(
31      jasmine.objectContaining({
32        level: logging.Level.SEVERE,
33      } as logging.Entry)
34    )
35  })
36})

Running the tests using npm run e2e should result in something similar to this animated image:

Protractor Property Based Test

My demo application does not represent a real business case, but I think you can imagine how you could, for instance, use that approach to write automated stress tests for inputs in your UI.

Conclusion

As already mentioned, it is important to note that property based testing does not — by any means — replace unit testing. Instead, it can help to detect issues in your program that traditional example-based tests probably would not have discovered. Additionally, it can help to explore the business logic of a legacy application without having to write many example-based tests.

But you should consider that setting up the tests by creating the different custom generators and constraining the input values takes some time and effort.

I will never share any of your personal data. You can unsubscribe at any time.

If you found this article helpful.You will love these ones as well.
Use Nitro as Mock Server Image

Use Nitro as Mock Server

How To Easily Write And Debug RxJS Marble Tests Image

How To Easily Write And Debug RxJS Marble Tests

How I Write Marble Tests For RxJS Observables In Angular Image

How I Write Marble Tests For RxJS Observables In Angular

Unlocking the Power of v-for Loops in Vue With These Useful Tips Image

Unlocking the Power of v-for Loops in Vue With These Useful Tips