phplaravellaravel-5phpunit

Laravel Model Factories with Tests


I'm trying to create a team and then add that team to a game and then add that game to the event, however, when the game is created it auto generates an event to attach to the event. In normal circumstances this is fine but because I'm testing the team's joined at compared to the event their first game is on then I need to create an event with a specific date. I can't create the event first because the game has to be created first to be able to add it to it.

Does anyone have a suggestion on correcting my logic so that I can get a game and event created correctly?

I don't know what I should need to do for this edge case.

/** @test */
public function a_team_with_a_game_after_they_started_the_season_cannot_have_their_joined_at_date_after_their_first_match()
{
    $team = factory(Team::class)->create(['joined_at' => '2017-10-08']);

    $game = GameFactory::create([], [$team]);
    $event = EventFactory::create(['date' => '2017-10-09'], null, $game);

    $validator = new BeforeFirstGameDate($team);

    $this->assertFalse($validator->passes('joined_at', '2017-10-10'));
    $this->assertEquals('The joined at date cannot be AFTER the team\'s first game.', $validator->message());
}

Factories

<?php

use App\Models\Game;
use App\Models\Team;

class GameFactory
{
    public static function create($overrides = [], $teams = [])
    {
        $match = factory(Game::class)->create($overrides);

        self::addTeamsForGame($teams, $game);

        return $game;
    }

     /**
     * @param $teams
     * @param $game
     */
    public static function addTeamsForGame($teams, $game)
    {
        $teamsForGame = [];

        $numberOfTeamsToAdd = $numberOfTeams - count($teams);

        if ($numberOfTeamsToAdd) {
            $teamsForMatch = factory(Team::class, $numberOfTeamsToAdd)->create();
            array_push($teams, $teamsForGame);
        } else {
            array_push($teams, $teamsForGame);
        }

        $match->addTeams($teamsForGame);
    }
}


<?php

use App\Models\Event;

class EventFactory
{
    public static function create($overrides = [], $totalNumberOfGames = 8, $games = [])
    {
        $event = factory(Event::class)->create($overrides);

        $numberOfGamesToAdd = $totalNumberOfGames - count($games);
        $gameToStartAt = count($games) + 1;

        foreach (array_wrap($games) as $game) {
            $game->addToEvent($event);
        }

        for ($gameNumber = $gameToStartAt; $gameNumber <= $numberOfGamesToAdd; $gameNumber++) {
            GameFactory::create(['event_id' => $event->id, 'game_number' => $gameNumber]);
        }

        return $event;
    }
}


$factory->define(App\Models\Game::class, function (Faker\Generator $faker) {
    static $order = 1;
    return [
        'event_id' => function () {
            return factory(App\Models\Event::class)->create()->id;
        },
        'game_number' => $order++,

    ];  
});

$factory->define(App\Models\Event::class, function (Faker\Generator $faker) {
    $name = $faker->sentence;
    return [
        'name' => $name,
        'slug' => str_slug($name),
        'date' => $faker->dateTimeBetween('-10 years'),
    ];
 });

Solution

  • Note: The source code shown below is based on some assumptions since the specific implementation of models like for instance Event, Game and Team were not given in the original question. Consider adding a Git repository containing the sources to the question to get more specific answers that reflects your implementation in more detail.

    First, some general remarks to tests:

    That being said and putting the focus more on the unit part in "unit test" your tests might look like - focussing on testing single, separated units and not a combination of them:

    /** @test */
    public function eventIsCreated()
    {
      ...
      static::assertSame($expectedEvent, EventFactory::create(...));
    }
    /** @test */
    public function teamIsCreated()
    {
      ...
      static::assertSame($expectedTeam, TeamFactory::create(...));
    }
    /** @test */
    public function gameIsCreated()
    {
      ...
      static::assertSame($expectedGame, GameFactory::create(...));
    }
    /** @test */
    public function beforeFirstGameDateValidatorRejectsLateApplications()
    {
      ...
    }
    

    The mentioned test case for BeforeFirstGameDate validation might just look like the following then, using prophecies - the instance to be tested is named as $subject to make it clear, what's the subject to be tested (as common best practice in writing tests):

    /**
     * @test
     */
    public function beforeFirstGameDateValidatorRejectsLateApplications()
    {
        $event = $this->prophesize(Event::class);
        $game = $this->prophesize(Game::class);
        $team = $this->prophesize(Team::class);
    
        $event->getGames()->willReturn([$game->reveal()]);
        $game->getTeams()->willReturn([$team->reveal()]);
        $team->get('joined_at')->willReturn('2017-10-08');
    
        $subject = new BeforeFirstGameDate($team->reveal());
    
        static::assertFalse(
            $subject->passes('joined_at', '2017-10-10')
        );
    }
    

    This way, your Event, Game and Team models don't rely on any factory implementation anymore, but simulate properties and behavior using prophecies. Thus, in case factories get changed or refactored you only have to adjust those tests that assert object reconsitution - the mentioned beforeFirstGameDateValidatorRejectsLateApplications can be skipped since it does not have a hard dependency on these factories for instance.

    As mentioned in the beginning the methods and properties for Event, Game and Team are just assumptions since the real implementation was unknown at the time of writing this answer.

    References: