Unit Testing Tutorial - SeoulSKY/safe-zone-system GitHub Wiki

Definitions

  • Cyclomatic complexity

    Cyclomatic complexity is a software metric used to indicate the complexity of a program. It is a quantitative measure of the number of linearly independent paths through a program's source code.

    Source: https://en.wikipedia.org/wiki/Cyclomatic_complexity

  • Test double

    In automated unit testing, it may be necessary to use objects or procedures that look and behave like their release-intended counterparts, but are actually simplified versions that reduce the complexity and facilitate testing. A test double is a generic (meta) term used for these objects or procedures.

    Types of Test Doubles:

    • Test stub — used for providing the tested code with "indirect input".
    • Mock object — used for verifying "indirect output" of the tested code, by first defining the expectations before the tested code is executed.
    • Test spy — used for verifying "indirect output" of the tested code, by asserting the expectations afterwards, without having defined the expectations before the tested code is executed. It helps in recording information about the indirect object created.
    • Fake object — used as a simpler implementation, e.g. using an in-memory database in the tests instead of doing real database access.
    • Dummy object — used when a parameter is needed for the tested method but without actually needing to use the parameter.

    Source: https://en.wikipedia.org/wiki/Test_double

Best Practices

Source: https://www.testim.io/blog/unit-testing-best-practices/

  1. Tests should be fast
    • Why?
      • If tests aren't fast, developers won't run them every time they make changes which defeats the point of unit testing
    • How?
      • Keep tests as simple as possible
      • Don't make tests depend on other tests
      • Mock external dependencies
  2. Tests should be simple
    • Why?
      • Reduce the chance of bugs in your tests
      • Higher readability and maintainabilty of tests
    • How?
      • Measure the cyclomatic complexity of tests using a linting tool and try to keep it low
  3. Tests shouldn't duplicate implementation logic
    • Why?
      • If your test has similar logic to the code it's testing, you may have passing tests for code that doesn't really work
    • How?
      • Keep tests simple, even if you could implement them programmatically; resist making them fancy
  4. Tests should be readable
    • Why?
      • Tests can double as a form of runnable documentation
      • Higher maintainability
    • How?
      • Avoid use of magic numbers/strings
      • Have only one logical assertion per method
      • Use BDD-style Given/when/then pattern in test cases
  5. Tests should be deterministic
    • Why?
      • Developers won't trust the tests unless they are deterministic
    • How?
      • Completely isolate your tests from other test cases, environmental values (the current time, language setting of computer), and external dependencies (file system, network, APIs)
  6. Tests should be part of the build process
    • Why?
      • Prevents buggy code from being pushed, even if developers forget to run tests
  7. Distinguish between the many types of test doubles and use them appropriately
    • Why?
      • Keep tests fast and reliable
  8. Adopt a sound naming convention for your tests
    • Why?
      • Makes tests easier to understand
  9. Don't couple your tests with implementation details
    • Why?
      • Changing the code implemetation shouldn't make your tests break
    • How?
      • Focus on the input and output of a function when writing tests, rather than its internal logic

JavaScript/Typescript Tests

Naming convention in Jest

For a file named main.ts/main.js, there should be a coresponding main.test.ts/main.test.js file.

Jest Usage/Example

In TDD, tests are written first, so create your test file: e.g. calc.test.ts

Import the file that does not yet exist: import { add } from "../src/calc";

To use Jest, you don't need to import anything in your test scripts.

Describe what you'll be testing:

describe("test add() function", () => {});

Add a test:

describe("test add function", () => {
    test("should return 15 for add(10,5)", () => {
        expect(add(10, 5)).toBe(15);
    });
});
  • test(name, fn, timeout) or its alias it(name, fn, timeout) is used to write a test case.

  • The expect function is used every time you want to test a value. You will rarely call expect by itself. Instead, you will use expect along with a "matcher" function to assert something about a value. In this case, toBe is the matcher function. There are a lot of different matcher functions, documented here, to help you test different things.

Now if you run Jest, you should have a failing test. Create calc.ts in the src directory and start implemeting the add function.

You can add more tests in the describe block, or outside the describe block.

calc.test.ts

import { add } from "../src/calc";

describe("test add function", () => {
	test("should return 15 for add(10,5)", () => {
		expect(add(10, 5)).toBe(15);
	});
	it("should return 5 for add(2,3)", () => {
		expect(add(2, 3)).toBe(5);
	});
});

Check out the sources for a more detailed version of this tutorial. Sources:

Python Tests

Python unittest naming conventions

Test functions must begin with test_. So tests for main() should be in a function called test_main().

Files containing tests should begin with test_. So tests for a file called main.py should be in a file called test_main.py.

Python unittest Usage

A testcase is created by subclassing unittest.TestCase.

The work in each test is done with one of the assert methods provided by unittest.TestCase.

Example:

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

When defined, the setUp() method is called before each test method in the class is called. Similarly, the tearDown() method is called after each test method.

Example:

import unittest

class WidgetTestCase(unittest.TestCase):
    def setUp(self):
        self.widget = Widget('The widget')

    def tearDown(self):
        self.widget.dispose()
        
    def test_default_widget_size(self):
        self.assertEqual(self.widget.size(), (50,50),
                         'incorrect default size')

    def test_widget_resize(self):
        self.widget.resize(100,150)
        self.assertEqual(self.widget.size(), (100,150),
                         'wrong size after resize')

Source: https://docs.python.org/3/library/unittest.html