Link Search Menu Expand Document

Unit Test

Table of contents

  1. UnitTest
  2. Helpers
    1. setUp
    2. tearDown
    3. Providers
    4. Skipping Tests
  3. Settings
    1. Silencing Passing Tests
      1. Global
    2. Exit On Failure
      1. Global
  4. Assertions
    1. assertEquals(value, value)
    2. assertNotEquals(value, value)
    3. assertNil(value)
    4. assertNotNil(value)
    5. assertType(value, value)
    6. assertTruthy(value)
    7. assertFalsey(value)
    8. assertSuccess(value)
    9. assertError(value)
  5. Mocking

UnitTest

Unit testing is a very important part of software development and something we as developers should always strive to complete. Dictu aims to make this slightly easier by having a unit test framework built within the language.

The framework works by first inheriting from the abstract UnitTest class provided to us within this module.

Note: If any tests fail then the suite will exit with an exit code of 1. This will allow CI/CD processes to easily detect a failing test suite.

from UnitTest import UnitTest;

class Test < UnitTest {

}

From here we add methods that will test different “units” of our code. This is done through a range of helper methods provided (see table of contents) to us by the UnitTest class.

from UnitTest import UnitTest;

def add(a, b) {
    return a + b;
}

class Test < UnitTest {
    testAddFunction() {
        this.assertEquals(add(2, 3), 5);
    }
}

Test().run();

The framework will search any methods within the class and execute any that begin with test, if a method within a class does not begin with test it is simply ignored. The final step is to ensure we run the unit test by calling the .run() method.

This will generate the following output.

file.du
    testAddFunction()
         Success.

Results:
    - 1 assertion(s) were successful.
    - 0 assertion(s) were failures.
    - 0 method(s) were skipped.

Helpers

Along with the list of assertion methods in the next section, there are a few helper methods to make test writing easier.

setUp

setUp is a method which is ran before a test method is executed. This is useful if you need some data (for example) to be present before running a test.

Note: If in use with providers setUp will run for every item passed into the array.

class Test < UnitTest {
    setUp() {
        // Code
    }
}

tearDown

tearDown very similar to setUp yet happens once a test has completed. Useful for cleaning up any data created in setUp or the test itself (examples).

Note: If in use with providers tearDown will run for every item passed into the array.

class Test < UnitTest {
    tearDown() {
        // Code
    }
}

Providers

Providers are special methods that allow you to pass data into tests so that they can be made slightly more dynamic and reduce the boiler plate needing to be written. They are denoted by appending a method name with Provider.

Providers can pass a single value and you can use this within your test, or if you return an array from a provider then the test will run for every item within the array.

from UnitTest import UnitTest;

def add(a, b) {
    return a + b;
}

class Test < UnitTest {
    testAddFunction(data) {
        this.assertEquals(add(data["val1"], data["val2"]), data["expected"]);
    }

    // This will not be ran as a test as it's marked as a provider
    testAddFunctionProvider() {
        return [
            {"val1": 1, "val2": 2, "expected": 3},
            {"val1": 2, "val2": 2, "expected": 4},
            {"val1": 3, "val2": 2, "expected": 5},
            {"val1": 4, "val2": 2, "expected": 6},
            {"val1": 5, "val2": 2, "expected": 7},
            {"val1": 6, "val2": 2, "expected": 8},
        ];
    }
}

Test().run();

As you can see, providers make it incredibly easy to add additional test cases without the need to actually modify your test code.

Output:

file.du
    testAddFunction()
         Success.
         Success.
         Success.
         Success.
         Success.
         Success.

Results:
    - 6 assertion(s) were successful.
    - 0 assertion(s) were failures.
    - 0 method(s) were skipped.

Skipping Tests

Sometimes we need to skip tests, be that because they’re temporarily broken or because they’re testing functionality that has been disabled for whatever reason.

This is done very simply by just appending _skipped to the method name.

from UnitTest import UnitTest;

def add(a, b) {
    return a + b;
}

class Test < UnitTest {
    testAddFunction_skipped() {
        this.assertEquals(add(2, 3), 5);
    }
}

Test().run();

Output:

file.du

Results:
    - 0 assertion(s) were successful.
    - 0 assertion(s) were failures.
    - 1 method(s) were skipped.

Settings

Silencing Passing Tests

The default setting is that it will output Success. for tests that pass, but sometimes that can cause finding errors or tests that fail slightly harder so we can turn this off to only show output from failed tests.

from UnitTest import UnitTest;

def add(a, b) {
    return a + b;
}

class Test < UnitTest {
    init() {
        // Remember to call the parent constructor!
        super.init();

        // Set it so only errors display
        this.onlyFailures = true;
    }

    testAddFunction(data) {
        this.assertEquals(add(data["val1"], data["val2"]), data["expected"]);
    }

    // This will not be ran as a test as it's marked as a provider
    testAddFunctionProvider() {
        return [
            {"val1": 1, "val2": 2, "expected": 3},
            {"val1": 2, "val2": 2, "expected": 4},
            {"val1": 3, "val2": 2, "expected": 5},
            {"val1": 4, "val2": 2, "expected": 6},
            {"val1": 5, "val2": 2, "expected": 7},
            // This will fail
            {"val1": 6, "val2": 2, "expected": 9},
        ];
    }
}

Test().run();

Output:

file.du
    testAddFunction()
         Line: 17 - Failure: 8 is not equal to 9.

Results:
    - 5 assertion(s) were successful.
    - 1 assertion(s) were failures.
    - 0 method(s) were skipped.

Global

The above shows us how we can silence passing tests for a single group (class) of Unit Tests but, if we wish to silence ass passing tests that would be a lot of verbosity added to every class. Instead we can use the global flag that is defined as a class variable on the UnitTest parent class.

from UnitTest import UnitTest;

UnitTest.forceOnlyFailures = true; // Set global flag

def add(a, b) {
    return a + b;
}

class Test < UnitTest {
    testAddFunction(data) {
        this.assertEquals(add(data["val1"], data["val2"]), data["expected"]);
    }

    // This will not be ran as a test as it's marked as a provider
    testAddFunctionProvider() {
        return [
            {"val1": 1, "val2": 2, "expected": 3},
            {"val1": 2, "val2": 2, "expected": 4},
            {"val1": 3, "val2": 2, "expected": 5},
            {"val1": 4, "val2": 2, "expected": 6},
            {"val1": 5, "val2": 2, "expected": 7},
            // This will fail
            {"val1": 6, "val2": 2, "expected": 9},
        ];
    }
}

Test().run();

Output:

file.du
    testAddFunction()
         Line: 17 - Failure: 8 is not equal to 9.

Results:
    - 5 assertion(s) were successful.
    - 1 assertion(s) were failures.
    - 0 method(s) were skipped.

Exit On Failure

Sometimes we may want our test suite to stop as soon as a test fails. This is done very similarly to silencing passing tests.

from UnitTest import UnitTest;

def add(a, b) {
    return a + b;
}

class Test < UnitTest {
    init() {
        // Remember to call the parent constructor!
        super.init();

        // Exit as soon as we get a failure
        this.exitOnFailure = true;
    }

    testAddFunction(data) {
        this.assertEquals(add(data["val1"], data["val2"]), data["expected"]);
    }

    // This will not be ran as a test as it's marked as a provider
    testAddFunctionProvider() {
        return [
            // This will fail
            {"val1": 1, "val2": 2, "expected": 5},
            {"val1": 2, "val2": 2, "expected": 4},
            {"val1": 3, "val2": 2, "expected": 5},
            {"val1": 4, "val2": 2, "expected": 6},
            {"val1": 5, "val2": 2, "expected": 7},
            // This will fail
            {"val1": 6, "val2": 2, "expected": 9},
        ];
    }
}

Test().run();

In this case the first dictionary in the data provider will cause our add test to fail, with exitOnFailure set to true it will exit after the first assertion.

Output:

file.du
    testAddFunction()
         Line: 17 - Failure: 3 is not equal to 5.

Global

The above shows us how we can silence passing tests for a single group (class) of Unit Tests but, if we wish to silence ass passing tests that would be a lot of verbosity added to every class. Instead we can use the global flag that is defined as a class variable on the UnitTest parent class.

from UnitTest import UnitTest;

UnitTest.forceExitOnFailure = true;

def add(a, b) {
    return a + b;
}

class Test < UnitTest {
    testAddFunction(data) {
        this.assertEquals(add(data["val1"], data["val2"]), data["expected"]);
    }

    // This will not be ran as a test as it's marked as a provider
    testAddFunctionProvider() {
        return [
            // This will fail
            {"val1": 1, "val2": 2, "expected": 5},
            {"val1": 2, "val2": 2, "expected": 4},
            {"val1": 3, "val2": 2, "expected": 5},
            {"val1": 4, "val2": 2, "expected": 6},
            {"val1": 5, "val2": 2, "expected": 7},
            // This will fail
            {"val1": 6, "val2": 2, "expected": 9},
        ];
    }
}

Test().run();

Output:

file.du
    testAddFunction()
         Line: 17 - Failure: 3 is not equal to 5.

Assertions

assertEquals(value, value)

This helper method ensures that both values passed in equal each other.

assertNotEquals(value, value)

This helper method ensures that both values passed in do not equal each other.

assertNil(value)

This helper method ensures that the value passed in is equal to nil.

assertNotNil(value)

This helper method ensures that the value passed in is not equal to nil.

assertType(value, value)

This helper method checks the type of the first value is equal to the type as a string.

this.assertType("Dictu", "string");
this.assertType(10, "number");

assertTruthy(value)

This helper method ensures that the value passed in would evaluate to true.

Note: This is not the same as equaling true.

assertFalsey(value)

This helper method ensures that the value passed in would evaluate to false.

Note: This is not the same as equaling false.

assertSuccess(value)

This helper method ensures that the value passed in is a Result type in a Success state.

assertError(value)

This helper method ensures that the value passed in is a Result type in an Error state.

Mocking

Mocking is an important part of unit testing as it allows us to focus on a single unit of code and correctly test that part is working. If we have a method that, for example, calls an external API and does some stuff with the response, we don’t care about testing the API request, we just want to assume it successfully returned some data and that our method correctly handles that data, that’s where mocking comes in.

mock() is a function within the UnitTest module that expects a class given to it and it will return an object with the same blueprint as the given class, however, all methods actually do nothing and by default return nil.

from UnitTest import mock;

class Test {
    print() {
        print("test!");
    }
}

Test().print(); // test!
mock(Test).print(); // <nothing>

A lot of the time, however, methods will return a value, following on from our API example, we can tell the mock object to return a given value if that method is called. To do that, mock can be given a dictionary as the second argument which is a mapping of method name to value returned.

import HTTP;

from UnitTest import mock;

class ExternalApi {
    someExternalCall() {
        const response = HTTP.get("https://dictu-lang.com/").unwrap();
        print("Calling API!");

        return response.statusCode;
    }
}

class SomeServiceClass {
    init(private api) {}

    print() {
        const statusCode = this.api.someExternalCall();

        if (statusCode == 200) {
            print("Success!");
        }
    }
}

const successObj = mock(ExternalApi, {"someExternalCall": 200});
SomeServiceClass(successObj).print(); // Success!

const errorObj = mock(ExternalApi, {"someExternalCall": 500});
SomeServiceClass(errorObj).print(); // <nothing>

This site uses Just The Docs, with modifications.