aedart's avatar

[L5] How to unit test Eloquent models in your workbenches

While being in the process of developing multiple packages (inside workbench), I found myself in the situation, where I needed to create unit tests for my Eloquent models - however, this has caused a variety of issues for me.

First, I cannot simple create new instances of my models, because they are unable to connect to a given database (which I have specified inside my "testing" environment)... Naturally, at this point I realised that "laravel" itself wasn't start, during my tests. So, I began to research how Laravel tests such models, etc.

I started using Codeception for creating tests, but found that I couldn't settle for making "functional" tests, for my models, but still needed proper "unit" tests for certain things (Also, there is yet not an official L5 Module for Codeception). Thus, still no clue on how to resolve my need.

Illuminate\Foundation\Testing\TestCase

After a while, I took a closer look at how unit-testing was performed, at the top-application-level, and found that I might be able to extend that given Testcase - Yet, no such luck. The Foundation-namespace, it not available as a separate package, and I was "forced" to include the entire L5 framework, inside my given workbench-package's vendor folder! This didn't seem like any good solution either!

Custom TestingTrait

Still being inspired by how the Foundation\Testing\TestCase worked, I decided to create my own testing-trait, one which could initialise the application and use the environment-specific configuration. However, this resulted in some major issues:

  1. I still needed the entire L5 framework inside my package's vendor folder
  2. Initialising the Illuminate\Foundation\Application still didn't read the configuration
  3. Attempting to access anything from the framework, e.g. Artisan::call(), didn't work either. The Facedes fail in line 208 (in short - they do not understand what/where to find artisan or anything else). This means that neither the aliases or default service providers are loaded / configured correctly.

In other words, anything that was Laravel-specific I couldn't use nor test from inside the workbench. This is most likely because the application has been initialised in the correct order / way. Yet, while this appears to be working in the top-level, it fails inside the workbench.

Other solutions

I searched the web for similar issues, yet it would seem that most people are settled for using Codeception's L4 tests - making use of "functional" tests - or simply stick to tests at the application-level. Neither are good enough solutions for me.

Did also find this package (https://github.com/orchestral/testbench#working-with-workbench). But after a closer look at his implementation, I got scared; the hard-coded application aliases and providers (Orchestra\Testbench\Traits\ApplicationTrait), might not always be in sync with L5. In case anything changes inside Laravel, his packages might not be up-to-date, causing in yet another dependency to worry about.

But how to test Eloquent, when working inside workbench packages

To sum up, I am still confused on how I can unit-test Eloquent models, when local inside my workbench packages.

Is there anyone that have a simple solution, which doesn't entail hacking the living crap out of L5, or having to depend on several other packages, just to gain access to Laravel's core features, such as Facades, Db?

0 likes
6 replies
aedart's avatar

Migrations, part of the same problem...

I forgot to mention, that when working with Eloquent models, migrations are also a part of the challenge, when working inside a workbench-package. By this I means, you are required to migrate from specific packages, in order to test specific models. While this can be done manually, via artisan, from the top-level, its too time consuming. This is also something that I wish / must be able to do automatically, from the unit-tests' setup / before / config methods.

Nevertheless, having access to Laravel's core features is still a prerequisite. One which I still have not yet found a good solution for.

LukasNeicelis's avatar
Level 1

Hi, fundamentally you should not test eloquent model itself at all. Eloquent models have their own tests. If you want to test some particular requests to your database via eloquent models or query builder or plain sql, you should wrap those requests to some abstraction (e.g. repositories) and then you should use functional/intergartional tests to those abstractions (repository) methods.

And if you want to write functional/integrational tests for workbench package you should use package you mentioned "orchestral/testbench"

aedart's avatar

Why I meant by "Eloquent models";

use Illuminate\Database\Eloquent\Model;

class Dog extends Model {
// ... implementation not shown
}

Now, imagine that I added some kind of custom query method, for e.g. finding all dogs that are injured - that method would be nice to be able to test, in a unit-test (my preference), rather than relying only on higher level tests.

Anyways, I ended up re-creating a trait, which I can use with Codeception (or other frameworks), built upon the orchestra/testbench package. Thus far, this works okay - but still a bit confused regarding where it is reading its configuration from, - but guess that's a topic outside this scope. My trait looks like this:

namespace XXXYYYZZ\Testing;

use Orchestra\Testbench\Traits\ClientTrait;
use Orchestra\Testbench\Traits\ApplicationTrait;
use Orchestra\Testbench\Traits\PHPUnitAssertionsTrait;

/**
 * Testing Trait
 *
 * 
 * <b>usage</b>
 * <pre>
 *    1) Use this trait, in your test-case / unit-test... etc
 *    2) Invoke startLaravelApplication() in your setUp()/_before() methods
 *    3) Invoke stopLaravelApplication() in you tearDown() / _after() methods
 * </pre>
 * 
 * <b>NB:</b> This trait assumes that the following package is available:
 * @see https://packagist.org/packages/orchestra/testbench
 */
trait TestingTrait {

    /**
     * Use the traits from Orchestra
     */
    use ApplicationTrait, ClientTrait, PHPUnitAssertionsTrait;

    /**
     * Starts the application and ensures that the "testing"
     * application environment is set
     * 
     * @return void
     */
    public function startLaravelApplication(){
    if(!$this->app){
        $this->refreshApplication();
    }
    }

    /**
     * "Stops" the application and ensures that its flushes its
     * bindings and resolved instances
     * 
     * @return void
     */
    public function stopLaravelApplication(){
    if($this->app){
        $this->app->flush();
    }
    }

    /**
     * Return instance of the current application
     * 
     * @return \Illuminate\Foundation\Application
     */
    public function getLaravelApplication(){
    return $this->app;
    }

    /**
     * <b>Re-implemented from Orchestra\Testbench\TestCase</b>
     * 
     * Define environment setup.
     *
     * @param \Illuminate\Foundation\Application $app
     * @return void
     */
    protected function getEnvironmentSetUp($app) {
    // Define your environment setup.
    }
}
LukasNeicelis's avatar

I don't think that your aim is to test if query is built correctly, is it? If so then you could easily test it with unit tests, however you will need to pull one of illuminate packages as dependency which has query builder.

aedart's avatar

Well, since I am new at using Laravel, there are many things I have yet to learn. Normally, when I created some kind of Model, I do spend a bit of time writing tests, ensuring my custom queries are able to fetch / execute what their are supposed to. So, yes... I suppose that is among the things I wish to achieve.

Nevertheless, thus far, I have had quite some trouble getting everything from Facades to other Laravel specific code to run, inside my tests, while situated inside a workbench-package.

In other words, writing unit-tests (regardless of which kind) is very easy, when located at the application level (the root, if you will), especially when you simple extend the Functional/TestCase class. But when you are inside a package, then it becomes frustrating that you cannot access certain Laravel specific code, such Facades. As an example;

// Located inside e.g. MyVendorName/MyPackage/tests/MyTest

public function testArtisanMigration(){
         Artisan:call('migrate'); // --> Will fail, because Artisan is unknown

         // ... Assertion(s) not shown
}

Supposed that I do have a "use" statement, which the autoloader can resolve for the Artisan-package/class, I still run into trouble, because I doesn't know about the "call" method!

In addition to this, imagine that you have a "database.php" configuration, inside your testing environment folder. In order for that specific configuration to be read, I have no clue what I am supposed to do, e.g. if I need to run tests inside a specific database, when inside a package.

The previously mentioned package from Orchestra did help solve many of my issues - yet I do find it overkill that the entire Laravel Framework needs to be included inside every package, in which I have Models to be tested - or other Laravel depended code.

Anyways, thanks for all the replies, ... any thoughts about this subject are welcome, and will be taken carefully under advisement :)

Please or to participate in this conversation.