Setup and Teardown the Database Once Per Test Suite in PHPUnit

I'm working on a series of integration tests where I want to set up and reset the database for each run. This could easily be done in the setUp and tearDown methods, but doing the full db each time is slow. Yes, I could just do the tables I need, but I was curious, and now I don't have to worry about which tables are setup in my testing DB. In this example, I'm using Laravel's migrations and SQLite as the test DB.

UPDATE 05/05/2016: As Sebastian points out below (thanks!) there is a much more appropriate way. Using setUpBeforeClass and tearDownAfterClass we achieve the same effect

public static function setUpBeforeClass()
{
    parent::setUpBeforeClass();
    exec('php artisan migrate --database sqlite_test');
}

public static function tearDownAfterClass()
{
    exec('php artisan migrate:reset --database sqlite_test');
    parent::tearDownAfterClass(); 
}

PHPUnit has listeners that you can tap into at various parts of your tests' lifecycle. I'm particularly interested in when a specific suite starts and ends. We'll need to do 2 things:

  1. Create our Listener
  2. Register this Listener in phpunit.xml

The listener is as follows:

class FullDBListener extends PHPUnit_Framework_BaseTestListener
{
    protected $suites = ['UserIntegrationTest', 'AccountIntegrationTest'];

    public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        if (in_array($suite->getName(), $this->suites)) {
            exec('php artisan migrate --database sqlite_test');
        }
    }

    public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        if (in_array($suite->getName(), $this->suites)) {
            exec('php artisan migrate:reset --database sqlite_test');
        }
    }
}

This does a few things:

  1. Stores the class names of each suite we want to run the db migrations for
  2. In startTestSuite it checks to see if we're in the right suite
  3. If we are, run an artisan migration on our test db
  4. In endTestSuite it checks to see if we're in the right suite
  5. If we are, run an artisan migration:reset on our test db

Next, we need to make PHPUnit aware of this listener. Update your path accordingly

<listeners>
    <listener class="FullDBListener" file="./tests/app/FullDBListener.php"></listener>
</listeners>

That's it!

I do do some basic teardown in my test suite's setUp method to give me a blank slate. This has proven faster then doing the migrations each time

public function setUp()
{
    parent::setUp();

    User::truncate();
}

I hope this helped you, if you have any questions, let me know!

Trying to Mock a Self Booting Laravel Model Trait

Bootable Model traits are pretty nifty. I'm using them to register certain events for the models using my Trait. However, I've run into an issue trying to mock models that are using the trait. Specifically, when a Mockery version of the model is instantiated, it's boot code agrees that it should have a bootMyTrait method, but can't find it when it tries to call it.

Sample Repository for the below, with commands to reproduce.

As an example, here is a trait:

namespace App;
trait MyTrait
{
    public static function bootMyTrait()
    {
        print("Booting MyTrait\n");
    }
}

And a model using it:

namespace App;
use Illuminate\Database\Eloquent\Model;
class MyModel extends Model
{
    use MyTrait;
}

Instantiating the model regularly works fine. This shows the desired output:

$model = new MyModel();

However, trying to mock this model does not cooperate. This:

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;


class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testTraitBooting()
    {
        $model = $this->getMock('App\MyModel');
    }
}

Fails. Adding some debugging to Eloquent:

    /**
     * Boot all of the bootable traits on the model.
     *
     * @return void
     */
    protected static function bootTraits()
    {
        $class = static::class;

        foreach (class_uses_recursive($class) as $trait) {
            print("\nTesting that class: $class has method: " . $method = 'boot'.class_basename($trait) . " because of Trait: $trait\n");
            if (method_exists($class, $method = 'boot'.class_basename($trait))) {
                print("Class: $class has method: $method \n");
                try {
                    forward_static_call([$class, $method]);
                } catch (\PHPUnit_Framework_MockObject_BadMethodCallException $e) {
                    print("Class: $class failed calling $method\n");
                    throw $e;
                }
            }
        }
    }

Gives us this failure:

PHPUnit 5.1.0 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)
Testing that class: Mock_MyModel_9ee820db has method: bootMyTrait because of Trait: App\MyTrait
Class: Mock_MyModel_9ee820db has method: bootMyTrait
Class: Mock_MyModel_9ee820db failed calling bootMyTrait


Time: 129 ms, Memory: 18.00Mb

There was 1 error:

1) ExampleTest::testTraitBooting
PHPUnit_Framework_MockObject_BadMethodCallException:

mock-bootable-laravel-model-trait/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:326
mock-bootable-laravel-model-trait/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:309
mock-bootable-laravel-model-trait/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:296
mock-bootable-laravel-model-trait/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:277
mock-bootable-laravel-model-trait/tests/ExampleTest.php:16

I've also tried creating the mock a few different ways. Using DatabaseSoftDeletingTraitTest as an example:

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Mockery as m;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testTraitBooting()
    {
        $mock = m::mock('App\MyModel');
        $mock->shouldReceive('bootMyTrait')->once();
    }
}

But here, bootMyTrait is never called:

PHPUnit 5.1.0 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 149 ms, Memory: 19.25Mb

There was 1 error:

1) ExampleTest::testTraitBooting
Mockery\Exception\InvalidCountException: Method bootMyTrait() from Mockery_0_App_MyModel should be called
 exactly 1 times but called 0 times.

mock-bootable-laravel-model-trait/vendor/mockery/mockery/library/Mockery/CountValidator/Exact.php:37
mock-bootable-laravel-model-trait/vendor/mockery/mockery/library/Mockery/Expectation.php:271
mock-bootable-laravel-model-trait/vendor/mockery/mockery/library/Mockery/ExpectationDirector.php:120
mock-bootable-laravel-model-trait/vendor/mockery/mockery/library/Mockery/Container.php:297
mock-bootable-laravel-model-trait/vendor/mockery/mockery/library/Mockery/Container.php:282
mock-bootable-laravel-model-trait/vendor/mockery/mockery/library/Mockery.php:142
mock-bootable-laravel-model-trait/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:122

So, I can move the code I'm doing in the boot method to a ServiceProvider, but then I'll need to register each Model that uses the trait. This feels dirty, and using the boot method seems appropriate. So I think I've either hit a bug, or am Mocking the trait-using-model incorrectly. I've looked at getMockForTrait but I also need the mocked instance to extend Eloquent (a few of the trait's methods call eloquent methods)

If anyone sees something I missed, much appreciated

Update Apr, 22 (Friday) 2016-04-22 03:28 PM

Thanks to Marcin, we have a solution!

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Mockery as m;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testTraitBooting()
    {
        $mock = m::mock('App\MyModel')->makePartial();
        $mock->shouldReceive('bootMyTrait')->once();
        $mock->__construct();
    }
}

Testing UIViewController Transitions with Quick and Swift

The examples below are using the Quick test framework, but the principals we're going to talk about can be used in any setup.

We have some complicated logic further down our user registration flow. I want to make sure that the right UIViewControllers are appearing when they are supposed to. I've been reading a few different approaches on how to handle this. Below is where I've ended up, and I'm pretty happy with it.

For simplicity's sake, I'm going to show how to test if the user has tapped "Login" or "Register" on our opening screen. We can assume we have a LoadingViewController which represents the first screen. On this screens are two buttons, which correspond to these actions:

@IBAction func tapLogin(sender: AnyObject) {
    self.navigationController?.pushViewController(self.storyboard?.instantiateViewControllerWithIdentifier("loginview") as! LoginViewController, animated: true)
}

@IBAction func tapRegister(sender: AnyObject) {
    self.navigationController?.pushViewController(self.storyboard?.instantiateViewControllerWithIdentifier("registerview") as! RegisterViewController, animated: true)
}

We also have a UINavigationController taking care of the view hierarchy. I also want to use the transitions and IDs already set up in the Storyboard.

Let's start by referencing the UINavigationController and the UIViewController we're going to start with.

class LandingScreenUITests: QuickSpec {
    override func spec() {
        describe("Landing Screen") {

            var viewController: LoadingViewController!
            var navigationController: UINavigationController!


        }
    }
}

Next, we're going to instantiate the storyboard, both controllers, and push the VC onto the view hierarchy

beforeEach {
    let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
    viewController = storyboard.instantiateViewControllerWithIdentifier("loadingview") as! LoadingViewController
    navigationController = storyboard.instantiateViewControllerWithIdentifier("navigationcontroller") as! UINavigationController

    navigationController.pushViewController(viewController, animated: false)

    let _ =  viewController.view
}

Two gotchas to watch out for here:

  1. Make sure Main.Storyboard is available in your Test target
  2. Make sure to use the self.dynamicType bundle above

Missing either of these may result in a weird casting error:

Could not cast value of type 'MyApp.LoadingViewController' (0x10b9f5e50) to MyAppUITests.LoadingViewController' (0x11f894370).

Finally, lets call the appropriate methods on our initial UIViewController, and test the type of the UIViewController that has been put on the top of the hierarchy. Note: I use toEventually here to wait for the transition animation

describe("User wants to log in") {
    it("taps Login") {
        viewController.tapLogin(self)
        expect(navigationController.visibleViewController).toEventually(beAKindOf(LoginViewController))
    }
}

describe("User wants to register") {
    it("taps Register") {
        viewController.tapRegister(self)
        expect(navigationController.visibleViewController).toEventually(beAKindOf(RegisterViewController))
    }
}

For reference, here is the whole test class together:

import Foundation
import Quick
import Nimble
@testable import MyApp

class LandingScreenUITests: QuickSpec {
    override func spec() {
        describe("Landing Screen") {

            var viewController: LoadingViewController!
            var navigationController: UINavigationController!

            beforeEach {
                let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
                viewController = storyboard.instantiateViewControllerWithIdentifier("loadingview") as! LoadingViewController
                navigationController = storyboard.instantiateViewControllerWithIdentifier("navigationcontroller") as! UINavigationController

                navigationController.pushViewController(viewController, animated: false)

                let _ =  viewController.view
            }

            describe("User wants to log in") {
                it("taps Login") {
                    viewController.tapLogin(self)
                    expect(navigationController.visibleViewController).toEventually(beAKindOf(LoginViewController))
                }
            }

            describe("User wants to register") {
                it("taps Register") {
                    viewController.tapRegister(self)
                    expect(navigationController.visibleViewController).toEventually(beAKindOf(RegisterViewController))
                }
            }
        }
    }
}

Many thanks to the blog posts referenced at the top of this post