TheUsernameHasAlreadyBeenTaken's avatar

Best approach for testable service layer

We're starting a project that's going to be really complex with a lot of routes and business logic. We don't want to use domain abstractions because nobody has experience using it, the risk is too high and the learning curve is steep.

Is this a bad approach?

We need to add some extra checks in some methods that are just safe checks but aren't part of the validation, or can be called without validation from some sources. It's really an exceptional situation thing to catch.

The tests would have to simulate the model state.

class ProductService
{
    public function find(int $productId): ?Product
    {
        return Product::find($productId);
    }

    public function create(array $product): Product
    {
        return Product::create($product);
    }

    // you'll have to find the product before calling this method but if you already have it avoids a unnecessary query
    public function createTags(Product $product, array $tags): Product
    {
        return $product->tags()->createMany([
            ['name' => 'A new tag.'],
            ['name' => 'Another new tag.'],
        ]);
    }

    public function associateReference(Product $product, Reference $reference)
    {
        if ($reference->isExpired) { throw new ExpiredReferenceException(); }

        return $product->reference()->associate($reference);
    }

    public function calculate(Product $product, int $number)
    {
        $result = 'example';

        // < some really complex calculations here >
        // it uses $product data directly
        // so the unit test would have to simulate the model state

        return $result;
    }

Feel free to share some code on how you structure all this and how you call it on Controller, Jobs, Commands.

0 likes
3 replies
martinbean's avatar

@theusernamehasalreadybeentaken It depends what you actually want to test, I suppose. For things like service classes, I usually create “integration” tests where I can create the models using factories, call methods on the service class, and then assert that the expected records were created/updated/deleted.

TheUsernameHasAlreadyBeenTaken's avatar

@martinbean We're adding integration tests but we'll have a lot of smaller methods with business logic that we want to test individually, they'll also be located in the same service and could be called from the service itself or others. Is it bad to have the model as parameters for these functions? For each test you would need to set the entire model state instead of passing parameters individually. It feels way cleaner when you just pass the model but I've seen some people complain about it.

martinbean's avatar

@TheUsernameHasAlreadyBeenTaken No, passing models as parameters is fine. I’d much rather pass objects to methods than just random IDs as integers or strings. As otherwise, you then just have to look up models in code that shouldn’t really be concerned with looking up models, and it may also be pointless if you’re looking up models when got the ID from a model instance in the first place, i.e.

// Get ID from a model
$id = $model->getKey();

// Call service class that does look-up on ID
// You're now looking up a model that you had an instance of already
$result = $service->someMethod($id);

Please or to participate in this conversation.