Testing

CSC-430

Phillip Wright

Testing

No matter how good you are, you will make mistakes. Probably more mistakes than you will ever know.

Testing II

Accordingly, it is necessary to not only do what you can to not make mistakes, but also to rigorously test your code to confirm that you were successful.

Testing III

You will still have bugs, though…

Unit Tests

Our first line of defense will be unit tests: small, directed tests aimed to check whether or not the smallest units of our code are working correctly.

Unit Tests II

Ideally, our development process would be driven by our unit tests.

This concept is referred to as Test Driven Development (TDD).

Test Driven Development

  • First Law: Don’t write production code until you have failing tests
  • Second Law: You may not write more test code than is necessary to create a failure
  • Third Law: You may not write more code than is necessary to fix the failures.

Test Driven Development II

Alternatively, we can restate these laws as the following list of steps:

  1. Write a test that fails
  2. Fix the failure with minimal changes
  3. Refactor and repeat

Test Driven Development III

If we do this well, then we know that all of the desired functionality has been tested, because, otherwise, it would not exist.

Also, we now have a safety net to protect us from regressions in the code base. If someone breaks our code, we should immediately know because of failing tests.

Test Driven Development IV

Unfortunately, it doesn’t always go so smoothly. For instance, we have to write our tests correctly, we have to test for unintended results as well as intended results (which is more difficult to do!), and, in some cases, we can’t write unit tests!

Swiss Cheese Model

In many fields, safety relies on the “Swiss Cheese” model:

There will always be holes in any layer of safety that you implement, but if you have enough layers in places, it greatly decreases the odds that a hole will exist in the entire system.

Swiss Cheese Model II

So, we try to use our brains to make sure we right good code, and we try to write good tests. We will fail at both, but will hopefully come closer to eliminating bugs than if we didn’t test at all.

Static Typing

Another simple layer to add, which a lot of people overlook, is using the type system to model your problems.

Static Typing II

Strong Static Typing can often give you similar benefits as unit testing.

  • The more you model your problem with types, the fewer invalid states will compile
  • …and the easier it is to refactor code with confidence

Static Typing III

In languages like Agda, Idris, Coq, you can model problems so completely that compilation “guarantees” correctness (sort of…), because invalid states can not be compiled.

In these languages, you are basically writing proofs.

Swiss Cheese Model III

This gives us:

  • Our coding skills
  • The type system/compiler
  • Unit tests

To combat bugs in our code

Unit Tests III

So, our unit tests will allow us to ensure code performs as expected, to modify code without causing regressions, and empowers us to refactor our and improve our code without fear.

But how do we write unit tests…

Unit Tests Are Code

Obviously. The point, though, is that you should use all of the principles you apply to writing good code in general when writing unit tests. You will need to maintain, fix, and improve tests over time just like all other code!

Unit Tests Are Code II

If you don’t follow best practices, updating tests will become such a nightmare that you will simply start deleting tests or just stop testing at all. Then regressions creep in and the game is over.

F.I.R.S.T

  • Fast: or they won’t be executed
  • Independent: isolate tests so it’s clear what causes failures
  • Repeatable: it should be trivial to test again and again
  • Self-Validating: test should be reduced to pass/fail without need for human validation
  • Timely: should be written before/along with code being tested

What To Test

First, we need to identify what a given unit should do.

This sounds simple, but it isn’t!

We can easily create tests for the common, obvious cases, but we must also test for the boundary cases.

Boundaries

For instance, if we are implementing a function to calculate the area of a rectangle given its height and width, what inputs should we test?

First, we would do something obvious like area(10,5)=50.

Boundaries II

But what happens if we are given negative values? What if we are given zeros?

We often forget about these types of inputs when writing code and just assume we will “obvious” inputs.

Boundaries III

In this particular case, the logic is simple enough that we can easily address the problem with a couple of tests and some branching in our code.

In general, though, we really need to focus on our domains.

Boundaries IV

In general, for an integer parameter, we should probably consider the following values:

  • positive values
  • negative values
  • zeros
  • one
  • negative one

Boundaries V

If we know we are using modular arithmetic, then we may want to test one representative from each congruence class as well.

If we are working with mod m, then maybe a good set of tests would be

-m-1, -m, …, -2, -1, 0, 1, 2, …, m, m+1

Boundaries VI

Basically, what you want to do is think about the input types, identify boundary values that are likely to cause different behavior, then test those boundary values, near those boundary values, and far away from the boundaries in all directions.

Boundaries VII

And don’t forget null values!

Boundaries VIII

So what would good inputs be for a method with a List input?

What about a method that takes a Point as input?

What about String inputs?

Assertions

Once we have decided what we need to test, we will write a single unit test for each interesting input and make an assertion about what its output should be.

Assertions II

Ideally, you should have one assertion per test. if not, all assertions should at least be testing one aspect of your code.

Another Problem

How can we test code that requires a database lookup?

Problem II

As we stated previously, we want to isolate the code being tested from the rest of the code base.

A database lookup, though, will require database libraries, data model code, a running database, proper setup of the data in the database, etc.

Isolate At The Boundary

The first step should be to isolate the database code from your code. You can do this by providing some interface that matches your data model and provides convenience methods for lookups.

Isolate At The Boundary II

All SQL, driver configuration, etc. will be hidden behind this interface, so your code that requires a lookup may simply call something like:

final User u = db.lookupUserById(10);

Isolate At The Boundary III

The db object can then be passed into our object or method and, since we have “programmed to an interface,” we can replace this object with any instance of the same interface.

Mocking

To properly isolate the our code from the database lookup when testing, we could provide a mock for the database lookup object.

In other words, we can create a class that matches the necessary interface, but does not actually interact with a database!

Mocking II

In our unit test, we will create a mock that is configured to return a specific instance of the User class when the lookup method is called with the value 10, and then we can write our test with the assumption that the lookup succeeds!

Mocking III

If we mock all dependencies in a method or class using this, approach, then we know that any errors are caused solely by the method or class logic itself, and not by dependencies.

Mocking IV

This also makes it easier to write tests, because a lot of code my be required to create real instances that you could use, but mocks are usually rather trivial to create.