David Llop

Back-End Developer

Written on

The importance of test consistency

Recently I've discovered the benefits of including feature and unit tests in my applications which helps me find bugs faster.

Today I found a small bug in one of the largest test suite our team has that I was unaware until today.

This test suite contains many tests that work with dates, like hotel bookings or daily price changes. I discovered the issue today when the month changed from June to July, let me explain that.

The user needs to be able to modify the price each week but only on certain days, for example every weekend of September. So the request receives a date_begin, a date_end and an array of weekdays:

/** @test */
public function only_weekdays_selected_are_created()
{
    // ... Preparing required variables and data on the database
    
    $this->actingAs($user)->post('/prices/create', [
        'price' => 46,
        'date_begin' => Carbon::now()->addMonth()->firstOfMonth(Carbon::MONDAY)->format('Y-m-d'),
        'date_end' => Carbon::now()->addMonth()->firstOfMonth(Carbon::FRIDAY)->format('Y-m-d'),
        'weekdays' => ['wed']
    ]);
    
    $this->assertDatabaseHas('price_days', [
        'day' => Carbon::now()->addMonth()->firstOfMonth(Carbon::WEDNESDAY)->format('Y-m-d'),
        'price' => 46
    ]);
}

Have you spotted the bug yet? Well, I'll write it down anyway to force myself to remember it.

The code is inserting the first Wednesday of the first complete week into database, which is August 8 2018, and the assertion is looking for the first Wednesday, which is August 1 2018.

After refactoring a bit, the issue is resolved but it forced me to handle this kind of assertions from another point of view. Since we are a small team of developers, and all of us needs to write tests on this project, I thought about creating some kind of helper (it has become an easily used word in programming, don't you think?) to handle that date-stuff for us.

I decided to create a class inside the tests folder and name it TestCarbon. This name is nothing close to definitive by the way, but is the first thing that came into my mind since it's only for testing and it's related to Carbon.

And this class looks very similar to this:


namespace Tests;

class TestCarbon
{

	public function today()
	{
	    return Carbon::now();
	}
	
	public function firstMondayOfNextMonth()
	{
		return self::today()->addMonth()->firstOfMonth(Carbon::MONDAY);
	}
	
	public function firstWednesdayOfNextMonth()
	{
		return self::firstMondayOfNextMonth()->addDays(2);
	}
	
	public function firstFridayOfNextMonth()
	{
		return self::firstMondayOfNextMonth()->addDays(4);
	}

}

Then after refactoring the test, again, it looks like this:

/** @test */
public function only_weekdays_selected_are_created()
{
    // ... Preparing required variables and data on the database
    
    $this->actingAs($user)->post('/prices/create', [
        'price' => 46,
        'date_begin' => TestCarbon::firstMondayOfNextMonth()->format('Y-m-d'),
        'date_end' => TestCarbon::firstFridayOfNextMonth()->format('Y-m-d'),
        'weekdays' => ['wed']
    ]);
    
    $this->assertDatabaseHas('price_days', [
        'day' => TestCarbon::firstWednesdayOfNextMonth()->format('Y-m-d'),
        'price' => 46
    ]);
}

I know I know, firstWednesdayOfNextMonth is lying (at least in August 2018), but firstWednesdayInsideFirstFullWeekOfNextMonth seemed too long for me 😅.

I showed the code to the team and they thought it was a good addition to our test suite. They started to use it with their own tests and adding methods for returning the dates they use the most. It turned out that I had a few tests with similar dates but not the same as the three used by TestCarbon. But with this class in mind, instead of creating new methods for new days, I modified those tests to match this dates and they're still passing. So I was able to remove some magic dates, so to speak, from the code.

Later, I thought about the Magic string problem and I remembered an app in which I have a few tests interacting with strings all the time. It was about colors and they were passed through the request like yellow, green, orange... This time though, while I was creating this TestColors class into this test suite, I realised and thought

why I'm not using this for the main app too?

So I moved that class to a folder freely named helpers and refactored both the code in the app and the tests. Now it's using this class which contains all the colors I need as constants such as const YELLOW = 'yellow';. This allowed me to remove a lot of magic strings from all the application and everything looks even more consistent now.

Spotted a mistake? Noticed something to improve? Feel free to edit this post on GitHub.


Special thanks 🙏 to Dieter Stinglhamber for the amazing code of this blog