The Art of Unit Testing

The basics

Definition

  • A unit test is an automated piece of code that invokes the unit of work being tested, and then checks some assumptions about a single end result of that unit.
  • A unit of work is the sum of actions that take place between the invocation of a public method in the system and a single noticeable end result by a test of that system.

A noticeable end result can be observed without looking at the internal state of the system and only through its public APIs and behavior

The invoked method returns a value

A noticeable change to the state or behavior of the system before and after invocation that can be determined without interrogating private state

There is a callout to a 3rd party system over which the test has no control

Properties of a good unit test

It should be automated and repeatable

It should be easy to implement

It should be relevant tomorrow

Anyone should be able to run it easily

It should run quickly

It should be consistent in its results

It should have full control of the unit under test

It should be fully isolated

When it fails, it should be easy to detect what was expected and determine how to pinpoint the problem

  • Integration testing is testing a unit of work without having full control over all of it and using one or more of its real dependencies, such as time, network, database and so on
  • A regression is one or more units of work that once worked and now don't
  • Control flow code is any piece of code that has some sort of logic in it. It has one or more of the following: if, loop, switch, calculations or any other type of decision-making code

Test-Driven Development

1⃣ Write test - at this state the tests will fail

2⃣ Make test pass by writing production code

3⃣ Run all tests - they should pass otherwise the solution should refactored

  • Refactoring means changing a piece of code without changing its functionality

Unit test structure

1⃣ Arrange objects

2⃣ Act on an object

3⃣ Assert that something is as expected

Best Practices

One test class per tested class

Name your tests clearly with the following model: [unitOfWork][Scenario][ExpectedBehavior]

Use Factory methods to reuse code in your tests, such as creating and initializing objects

Don't use setUp() and tearDown if you can avoid them, since they make tests less understandable

Unit Test Types

Value Based

Testing objects that relies on another object over which you have no control

The problem here is that your test cannot control what that dependency returns to your code under test or how it behaves

Definitions

  • External dependency is an object in your system that your code under test interacts with and over which you have no control.
  • A stub is a controllable replacement for an existing dependency in the system. By using a stub, you can test your code without dealing with the dependency directly.

You can't test something?

  • Add a layer that wraps up the calls to that something and then mimic that layer to your tests or
  • make that something replaceable (so that it is itself a layer of indirection).

How to test easily

1⃣ Find the interface or API that the object under test works against.

2⃣ If the interface is directly connected to your unit of work under test, make the code testable by adding a level of indirection hiding the interface.

3⃣ Replace the underlying implementation of that interactive interface with something that we have control over, a stub.

  • Seams are places in your code where we can plug in different functionality. Seams are what we get by implementing the Open-Closed Principle. Examples:
    • Adding a constructor parameter
    • Adding a public settable property
    • Make a method virtual, so it can be overridden
    • Externalize a delegate as a parameter or property so that it can be set from outside a class.

✅ You can refactor by introducing a new seam into it without changing the original functionality of the code

⚠ make sure that the resulting code does exactly the same thing it did before

Abstracting concrete objects into interfaces or delegates

Refactoring to allow injection of fake implementation of those delegates or interfaces

Inject stub implementation into a class under test

Inject a fake at the constructor level

Inject a fake as a property get or set

Inject a fake just before a method call

Solution: Use stubs

State Based

Interaction

The method under test is returning a value which is evaluated against an expected result

The method under test change the state of the system. So the state before and after the method invocation is evaluated

Is testing how an object sends messages to other objects

It's an action-driven testing, which means that you test a particular action an object takes.

Always choose an interaction testing as the last option

On interaction unit tests we are testing how many times a method called

❗ The alternative is the state-based integration test

  • A mock object is a fake object in your system that decides whether the unit test gas passed or failed. It does so by verifying whether the object under test called the fake object as expected. There is usually no more than one mock per test.
  • A fake is a generic term that can be used to describe either a stub or a mock object (handwritten or otherwise), because they both look like the real object. Whether a fake is a stub or a mock depends on how it’s used in the current test. If it’s used to check an interaction (asserted against), it’s a mock object. Otherwise, it’s a stub.

Mock vs Stubs

  • A stub can never fail a test
  • A mock is used to verify whether or not the test failed

Using stub
stub

Using mock
mock

  • Creating and using a mock object is much like using a stub, except that a mock will do a little more than a stub: it will save the history of communication, which will later be verified in the form of expectations.
  • In other words, if the fake object has a state which is used on assertions, this is called mock. On the other hand, if the fake object doesn't have a state and is just used to replace a direct dependency and simulate a particular case, then is called stub.

Only one mock object per test that is testing only one thing. All the other fake objects should be stubs

Having more than one mock objects usually means that you are testing more than one thing.

  • Overspecification is the act of specifying too many things that should happen that your test shouldn’t care about
  • An isolation framework is a set of programmable APIs that makes creating fake objects much simpler, faster, and shorter than hand-coding them.

Running Tests

Tests runs as part of the automated build process

Test run locally by developers on their own machines

Steps

  1. Make a small change
  1. Run all tests to make sure that you haven't broken any existing functionality
  1. make sure that your code can still integrate well and not break any other project you depend on
  1. create a deliverable package and deploy

Mapping test classes to code under test

you should be able to do easily

Look at a project and find all tests that relate to it

Look at a class and find all tests that relate to it

Look at a method and find all tests that relate to it

Mapping

One test class for each class under test

Having separate classes for each complex method being tested

Test Utility classes and methods

Factory methods for objects

System initialization methods

Object configuration methods

Methods that setup or read from external resources such as databases

Special assert utility methods

Separate Unit from Integration tests, because the developers should run them as often as they need and should not be time consuming

Pillars of Good Unit Test

Trustworthiness

Maintainability

Readability

Test only one concept

Avoid test logic

A unit test should contain a series of method calls with assert but not control flows not even try-catch

Most probably test more than one thing

Decide when to remove or change a test

Production bug

Remove or refactor test

Test bug

Semantic or API changes

Eliminate duplicate tests

Check if the correct test added

1⃣ Comment out the production code

2⃣ Run all tests

3⃣ If nothing failed a test should
be added

4⃣ Write a failing test

5⃣ Uncomment the production code
and run again all the tests.
All should pass

Enforce test isolation

❌ Constrained tests order

❌ Hidden test calls

❌ Share state corruption

❌ Tests should not know the results of other tests

Use setup methods in a maintainable way

Remove duplication

Only the public contract is all that you need to care about

Avoid overspecification

private methods provide a hidden functionality and may change in the future which leads to breaking test

👉🏻 if a private method is worth testing it might worth to make it public or static

❌ assert purely the internal state

❌ multiple mocks or stubs

❌ assume specific order or exact string matches when it isn't required

Naming unit tests

1⃣ name of the method under test

2⃣ the scenario

3⃣ the expected behavior

Naming variables

Create good assert message

Separate actions from assert

avoid writing your own custom assert message

Components Priority to add tests

factors

logical complexity

dependency level

priority

rate each component for these factors 1(low) - 10(high)

create a graph (logic vs dependencies) on the rating and decide which components to exclude (by setting a logical threshold at 2 or 3) and from which component to start

(hard - first) Choose the more complex and harder to test

If team has experience

(easy - first) Choose the more complex but easier to test

If you are new in unit testing and need time to understand write tests

Design Guidelines for more testable code

make methods virtual by default (avoid final on methods)

use interface based desing

make class nonsealed by default (avoid final on classes)

Avoid instantiating concrete classes inside methods with logic

Get instances of classes for helper method factories IoC

Avoid direct calls to static methods

Prefer calls to instance methods that later calls statics

Avoid constructors and static constructors that do logic

Separate singleton logic from singleton holders

image

image