Search code examples
laravelunit-testingeloquentnaming-conventionsstandards

Unit, Integration or Feature Test?


A simple question: How do you differentiate between a feature, unit and integration test?

There are a lot of differing opinions, but I'm specifically trying to determine how to organise a Laravel test which touches a model's relationship. Here is an example if some PHP code which would require testing:

public function prices()
{
    return $this->hasMany(Prices::class);
}


public function getPriceAttribute($)
{
    return $this->prices()->first() * 2;
}

The test descriptions as I understand them (feel free to correct me):

Unit test

  • Tests the smallest part of your code
  • Does not touch the database
  • Does not interact with any other part of the system

Integration test

  • Tests part of the system working together
  • e.g controllers which call helper functions which need to be tested together

Feature test

  • Blackbox test
  • e.g. Call an api end point, see that it has returned the correct JSON response

Here is my issue given those descriptions:

  • My Laravel model test needs to test the smallest unit of code - the calculated accessor of a model, which makes it feel like a Unit test
  • But, it touches the database when it loads the model's relationship
  • It doesnt feel like an Integration test, because it is only touching other related models, not internal or external services
  • Other property accessor tests in Laravel would fall under Unit tests when they do not touch the database or the model's relationships
  • Separating these types of tests into integration tests would mean that a single model's tests against its properties are fragmented between integration and unit tests

So, without mocking relationships between models, where would my test belong?


Solution

  • If I’m interpreting your original question correctly, I think the killer constraint here is:

    So, without mocking relationships between models, where would my test belong?

    If mocking isn't allowed and you're required to touch a DB then, by your/and google's definition, it has to belong as an integration/medium size test :)


    The way I think of this is get price attribute functionality is separate from the DB. Even though it's in the model the prices could come from anywhere. Right now its a RDBMS but what if your org go really big and it split into another service? Basically, I believe, that the capability of getPriceAttributes is distinct from the storage of attributes:

    public function getPriceAttribute($)
    {
        return $this->prices()->first() * 2;
    }
    

    If you buy into this reasoning, it creates a logical separation that supports unit tests. prices() can be mocked to returns a collection of 0, 1 & many (2) results. This test can be executed as a unit tests (for orders of magnitude faster test execution (ie on the order of 1ms vs potentially 10s or 100s of ms talking to a local DB)


    I am not familiar with php test ecosystem but one way to do this could be with a test specific subclass (not sure if the following is valid PHP :p ):

    class PricedModel extends YourModel {
       function __construct($stub_prices_supporting_first) {
         $this->stub_prices = $stub_prices_supporting_first;
       }
    
       public function prices() {
         return $this->stub_prices;
       }
    
    }
    

    tests

    function test_priced_model_0_prices() {
       p = new PricedModel(new Prices(array()));
       assert.equal(null, p.getPriceAttribute());
    }
    
    function test_priced_model_1_price() {
       p = new PricedModel(new Prices(array(1)));
       assert.equal(2, p.getPriceAttribute());
    }
    
    function test_priced_model_2_prices() {
       p = new PricedModel(new Prices(array(5, 1)));
       assert.equal(10, p.getPriceAttribute());
    }
    

    The above should hopeuflly allow you to fully control input into the getPriceAttribute method to support direct IO-free unit testing.

    —— Also all the unit tests above can tell you is that you’re able to process prices correctly , it doesn’t price any feedback on if you’re able to query prices !