Pure functions and basic tests in Jasmine
Tags: javascript, testing, jasmine, beginners
Published: 2024-02-08 08:49:07

Pure functions are the perfect case for unit testing. For a given input, we always expect the same output—there is no internal state involved. Let’s take a look at a few examples and some simple tests that check if the methods work as expected.

## Jasmine

Jasmine is a unit test framework for JavaScript. It can run tests in both Node.js or on the browser. It’s used in the Angular framework, and it’s especially popular in projects based on Angular. It’s a solid choice for Vanilla JS projects, or projects based on other frameworks as well.

## Happy path testing

Happy path testing is when we test a method with inputs that it’s expected to work normally using. The arguments are valid and within reasonable ranges. Those tests check if the method does its job correctly—the test cases should be straightforward examples of how the method is explained in its documentation.

Pseudocode examples:
* `expect(add(2, 2)).toBe(4)`,
* `expect(concatenate(“Lorem”, “Ipsum”)).toBe(“LoremIpsum”)`

Those tests are meant to automatically catch it anytime the method key behavior is broken.

## Methods

Let’s see a few simple methods: simple operations that we might need in some real-world application.

All implementations are greatly simplified—all methods will break in an ugly way if only we provide them with parameters that differ slightly from what is expected. The code is far from being robust.

### greet

Method that greets the user with their name and surname:

```JS
export function greet(name, surname) {
  return `Hello ${name} ${surname}!`;
}
```

### shortDate

`shortDate` is a formatting method that takes a date object and returns it formatted as a short string. The code: 

```JS
export function shortDate(date) {
  return date.toISOString().substring(0, 10);
}
```

### ellipsis

`ellipsis` takes a long text string and an optional length parameter and then trims the string to fit within the limit:

```JS
export function ellipsis(text, length = 50) {
  if (text.length > length) {
    return text.substring(0, length) + "…";
  }

  return text;
}
```

### translate

A method that provides translated string values for a `key` and `lang` pair. It’s a simplified implementation of what could be replaced with more advanced translating libraries.

```JS
export function translate(key, lang = "en") {
  switch (lang) {
    case "en":
      switch (key) {
        case "hello":
          return "Hello!";
      }
    case "pl":
      switch (key) {
        case "hello":
          return "Cześć!";
      }
  }
}
```

### applyDiscount

Method to apply a percentage discount to a price. It can feel like overkill with this naive implementation, but later when we start investigating edge cases it will get much more interesting.  

```JS
export function applyDiscount(price, discountPercentage) {
  return price - (price * discountPercentage) / 100;
}
```

### calculatePrice

This one calculates the total price when buying multiple units at a given price. It will also get more complicated after adding interesting edge cases.

```JS
export function calculatePrice(unitPrice, quantity) {
  return unitPrice * quantity;
}
```

## Complete JS code

The complete JS code, `src/main.js`:

```JS
export function greet(name, surname) {
  return `Hello ${name} ${surname}!`;
}

export function shortDate(date) {
  return date.toISOString().substring(0, 10);
}

export function ellipsis(text, length = 50) {
  if (text.length > length) {
    return text.substring(0, length) + "…";
  }

  return text;
}

export function translate(key, lang = "en") {
  switch (lang) {
    case "en":
      switch (key) {
        case "hello":
          return "Hello!";
      }
    case "pl":
      switch (key) {
        case "hello":
          return "Cześć!";
      }
  }
}

export function applyDiscount(price, discountPercentage) {
  return price - (price * discountPercentage) / 100;
}

export function calculatePrice(unitPrice, quantity) {
  return unitPrice * quantity;
}
```

## Adding Jasmine tests

To add Jasmine, let’s start by converting the folder into an npm package:
```
$ npm init -y
Wrote to …/package.json:
…
```

Then we can install the Jasmine package:
```SH
$ npm install --save-dev jasmine

added 42 packages, and audited 43 packages in 2s

13 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

then we can generate folders and files used by Jasmine:

```SH
$ npx jasmine init
(no output)
```

This command generates the following:
* `spec/`—a folder where we can put `*.spec.js` files with the test, and
* `spec/support/jasmine.json`—a file with the Jasmine config.

## Unit tests

For the following unit tests, I’m focusing on the happy path only—I check if the result is as expected for reasonable inputs. The test should be self-explanatory, so let’s take a look at them:

```JS
import {
  greet,
  shortDate,
  ellipsis,
  translate,
  applyDiscount,
  calculatePrice,
} from "../src/main.js";

describe("main", () => {
  describe("greet", () => {
    it("should greet by name and surname", () => {
      expect(greet("Lorem", "Ipsum")).toEqual("Hello Lorem Ipsum!");
    });
  });

  describe("shortDate", () => {
    it("should format correclty date", () => {
      const date = new Date("2023-11-02");
      expect(shortDate(date)).toEqual("2023-11-02");
    });
  });

  describe("shortDate", () => {
    it("should shorten long text at 50 chars", () => {
      expect(
        ellipsis(
          "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque a faucibus massa."
        )
      ).toEqual("Lorem ipsum dolor sit amet, consectetur adipiscing…");
    });

    it("should leave short text unchanged", () => {
      expect(ellipsis("Lorem ipsum sin dolor")).toEqual(
        "Lorem ipsum sin dolor"
      );
    });

    it("should shorten to custom length", () => {
      expect(ellipsis("Lorem ipsum sin dolor", 10)).toEqual("Lorem ipsu…");
    });
  });

  describe("translate", () => {
    it("should translate to supported langauges", () => {
      expect(translate("hello", "en")).toEqual("Hello!");
      expect(translate("hello", "pl")).toEqual("Cześć!");
    });
  });

  describe("applyDiscount", () => {
    it("should lower the price accordingly", () => {
      expect(applyDiscount(120, 25)).toEqual(90);
      expect(applyDiscount(8, 50)).toEqual(4);
    });
  });

  describe("calculatePrice", () => {
    it("should find a price of many products", () => {
      expect(calculatePrice(4, 3)).toEqual(12);
      expect(calculatePrice(9, 0.5)).toEqual(4.5);
    });
  });
});
```
(file `spec/main.spec.js`)

## Running tests

To run the tests, we can add the following script to `package.json`:

```JSON
  ..
  "scripts": {
    "test": "jasmine"
  },
  …   
```

With this in place, `npm run test` runs our tests:
```
$ npm run test

> testing-example@1.0.0 test
> jasmine

Randomized with seed 76873
Started
........


8 specs, 0 failures
Finished in 0.004 seconds
Randomized with seed 76873 (jasmine --random=true --seed=76873)
```

## Summary

In this post, we took a look at a simple example of JS code and how it can be covered by unit tests. You can find the [complete code example on GitHub](https://github.com/how-to-js/testing-example/tree/jasmine-basic).