Unit tests towards better OOP

Svaťa Šimara
May 7 · 3 min read

Typical Shopping Cart

This article is about a shopping cart on a typical online shop.

We’ll have to save identification of the product and the amount, it’s easy.

Let’s forget how the cart is saved.

Let’s focus on data we need to display!

1st Trivial Test

Of course — what do we even test?

We have to start somehow, and trivial test is so obvious, we understand it without thinking.

We use a plain array because it is the most straight-forward representation in PHP. In the real project we would use a read DTO. We’ll keep the plain array in code for simplicity in this article.

Test Phases

Anyway even this trivial test contain logical parts we’ll repeat again in more complex tests.

$cart = new Cart();

$cart->add('ab123');

$expected = [
'ab123' => 1,
];

$actual = $cart->read();

Assert::assertSame($expected, $actual);

2nd More Same Products

Setup -> Actions -> Expected result -> Outer point of view -> Assertion.

Let’s focus on the expected part. We often hear that we should focus on business logic, and we often struggle to isolate it.

$expected = [
'ab123' => 2,
];

This is the business logic!

Something happens, and we don’t care how it’s done. We care what is done. And that is expressed by expected phase.

3rd Removing Product

Trivial, again. We fill the cart, and after removing, we expect the cart is empty.

I don’t recommend to assert what is in the cart after line 3. This is tested in a different test, and asserting the content even in this test would mean we have to change more tests when we change the logic. Also the test would be more complex and wouldn’t express obviously what it tests.

4th Removing Not Existing Product

What should happen?

Maybe we’ll ignore this situation. Maybe we’ll throw an exception. Either way the expected phase forces us to think about this case and forces us to make a decision.

Implementation

We don’t have to invent methods — the interface of the class — methods are defined by tests.

Implementation is, again, quite easy.

The goal of Outer point of view is that we can refactor/change/replace internals of the class without any restrictions. This is the biggest strength.

We can use it when refactoring the class for

  • Maximum performance
  • Easy persistence
  • Easy extensibility

“Make the change easy, then make the easy change”

Kent Beck

The Outer point of view allows us making changes.

Real life example

Shopping cart is a simple example to demonstrate steps in isolation. But this article would be just another trivial “Hello world” article without complex real life example.

Customer in our CMS might have multiple contacts. But one of his contacts is a special contact that represents the customer’s bio. If we change customer’s bio, the contact is changes as well.

So… how will we solve it? Will we add a special flag to the contact?
No! We, again, think how to store it.

Let’s start with tests first.

We described now the outer point of view to the object, let’s implement it.

Do we have only one option for the implementation? No! That’s the strength of tests + outer point of view. We might decide for different implementation because adding different features will be simpler.

Interface of the class didn’t change, tests didn’t change, all tests passes, but the logic behind the scenes is different.

The first implementation is easier to understand for me.

The second implementation is faster for reading contacts — there is no merging of arrays.

TL;DR

Writing tests forces us to

  • Behavior (Expected)
    $expected = ...;
  • Encapsulation (Outer point of view to object)
    $cart->add(...);
    $cart->read();

Encapsulated behavior is a sign of a great OOP.

Carvago Development

Lessons learned and stories from Carvago development team