Unit Testing Effectively

How to write proper unit tests to already written code.

Image for post
Image for post

These tests are also known as Characterization tests.

How to start?

1. What do you want to test?

final class MyBusinessLogic
{
private DependencyInterface dependencyInterface;
private ConcreteDependency concrete;

public function __construct(
DependencyInterface dependencyInterface,
ConcreteDependency concrete
) {
this.dependencyInterface = dependencyInterface;
this.concrete = concrete;
}

public function applySomeLogic(Input input): ReturnType
{
// black box responsible to create a ReturnType
// based on the given Input
return returnType;
}
}
final class MyBusinessLogicTest extends TestCase
{
public function testApplySomeLogic(): void
{
// I want to assert that "applying some logic"
// from MyBusinessLogic with the given Input
// I will receive a concrete ReturnType with a
// certain value as its property.
Something like:
returnType = myBusinessLogic.applySomeLogic(input);
assertEquals('expected', returnType.getProperty());
}
}

2. Instantiate the concrete/final class that you want to test.

final class MyBusinessLogicTest extends TestCase
{
public function testApplySomeLogic(): void
{
myBusinessLogic = new MyBusinessLogic(
this.createMock(DependencyInterface.class),
new ConcreteDependency(/* ... */)
);

// OR

myBusinessLogic = new MyBusinessLogic(
new class implements DependencyInterface {
public function getSomeValue(): string
{
return 'A value for your test';
}
},
new ConcreteDependency(/* ... */)
);

// ...
}
}

3. Call the method from that class providing the desired input.

input = new Input(/* ... */);
returnType = myBusinessLogic.applySomeLogic(input);

4. Assert the output with the expected value.

final class MyBusinessLogicTest extends TestCase
{
public function testApplySomeLogic(): void
{
myBusinessLogic = new MyBusinessLogic(
this.createMock(DependencyInterface::class),
new ConcreteDependency(/* ... */)
);

input = new Input(/* ... */);
returnType = myBusinessLogic.applySomeLogic(input);
assertEquals('expected', returnType.getProperty());
}
}

5. You might want to assert different expected values.

final class MyBusinessLogicTest extends TestCase
{
/**
*
@dataProvider providerApplySomeLogic
*/
public function testApplySomeLogic(
array concreteMapping,
string argInput,
string expectedValue
): void {
myBusinessLogic = new MyBusinessLogic(
this.createMock(DependencyInterface.class),
new ConcreteDependency(concreteMapping),
/* ... */
);

input = new Input(
argInput
/* ... */
);

actual = myBusinessLogic.applySomeLogic(input);
assertEquals($expectedValue, actual.getProperty());
}

public function providerApplySomeLogic(): Generator
{
yield [
'concreteMapping' => ['key' => 'value'],
'argInput' => 'something',
'expectedValue' => 'expected-value-A',
];

yield [
'concreteMapping' => ['key2' => 'value2'],
'argInput' => 'something-else',
'expectedValue' => 'expected-value-B',
];
}
}

Lastly: clean what you did.

myBusinessLogic = this.createBusinessLogic(concreteMapping);
input = this.createInput(argInput);
actual = myBusinessLogic.applySomeLogic(input);
assertEquals(expectedValue, actual.getProperty());

Just remember: keep your methods small.

Image for post
Image for post
My copy of the book “Working Effectively with Legacy Code”.

“Legacy Code is code without tests”

First, add tests, then do your changes.

Aka: Chema. I love what I do: software, music, and sport, but here I write mostly about software.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store