How to efficiently write unit test for complex business logic in Laravel

Viewed 499

I'm writing a room booking engine. There're 3 layers in play for the calculation : Rate Plan, Promotion, BookingQuery ( info of the request customers send in ).

When customers send in a booking query, containing info like booking date, check in date, check out date, number of people .. etc etc, this request will be passed through multiple layers of logic calculating from the Rate Plans and Promotions to give out a result ( whether or not room is available, how much is the price .. etc etc ).

I did write unit test for small calculation parts in my logic. However, I need to write an integration test, or a suite of integration test, containing hundreds of test cases of SPECIFIC input data and SPECIFIC expected output data ( not factory generated random data ). There are a lot of relationship within my entities, like $room->ratePlans, $ratePlan->promotions .. etc etc. Writing a single test case is exhausting enough, let alone hundreds.

What is the most efficient way to do this ?

1 Answers

It is a very open question, i have been in the same place like you. I feel the best approach is to create helpers and structure your data according. Testing mainly consist of 3 steps, called arrange, act and assert.

Let's first focus on arrange, this is about creating your data. Create helpers for it, so you don't have to repeat your self. Multiple object returns is what i use, to avoid calling to many helpers.

public function createRoom(array $roomData)
{
    $room = factory(Room::class)->create($roomData);

    $ratePlans = factory(RatePlan::class, 3)->create(['room_id' => $room->id]);

    $promotions = factory(Promotion:class, 3)->create();

    $room->promotions()->saveMany($promotions);

    return [$room, $ratePlans, $promotions];
}

As the scope of this task is very vague, it is just to show ideas. This can then easy be utilized in your test.

public testRoomCreate()
{
    [$room, $ratePlans, $promotions] = $this->createRoom();
}

The act part of a test, calling the application is straight forward. Use Laravels inbuilt methods for this, which are implemented in the Laravel testcase.

For assertion, a good approach is to again create standardized methods to help you in your process.

public function assertRoomResponse($response, Room $room)
{
    $response->assertJsonStructure(
        [
            'id',
            'type',
        ]
    );

    $response->assertJson(
        [
            'id' => $room->id,
            'type' => $room->type,
        ]
    );
}

public function assertRoomDatabaseHas(Room $room)
{
    $this->assertDatabaseHas('rooms', [
        'type' => $room->type,
    ]);
}

There probably needs to be more logic related to relations etc. but if this is implemented you probably will have more assertions than most applications. This will make you able to write tests, that are fairly straightforward as shown below, but will require some maintenance to keep em generic. This thou solve your main problem of having trouble writing many tests.

public testRoomCreateWithTypeDouble()
{
    [$room, $ratePlans, $promotions] = $this->createRoom(
        [
            'type' => 'double',
        ]
    );

    $response = $this->call('POST', 'api/room', [
        'type' => $room->type,
    ]);

    $this->assertRoomResponse($response, $room);
    $this->assertRoomDatabaseHas($room);
}
Related