More testable record insertion code I don't have much experience with testing but I'm trying to make it part of my routine to develop better software.

The class below is a service class in Laravel. It needs to save a record on the database, saving the customer's customer_id, a route (location_id_from and location_id_to) and which location to bill (location_id_bill). The billing location can be null, in which case it will try to determine which one to use.

The code works, but I don't see how to completely unit test the newJob function. From what I've learned so far, I could replace the static methods whereId with an injected repository in the constructor.

Regarding the validation, I'm currently doing the validation, and if it doesn't passes, it will throw an exception with all errors messages.

So:


how to test the actual job creation without touching the database
is there a better way to do the validation?


class Job
{

    public function newJob(int $customer_id, int $location_id_from, int $location_id_to, int $location_id_bill): \App\Models\Customer\Job
    {
        $location_from = Location::whereId($location_id_from);
        $location_to = Location::whereId($location_id_to);

        if ($location_id_bill === null) {
            $location_bill = Location::whereId($location_id_bill);
        } else {
            $location_bill = new Location();
        }

        $errors = $this-&gt;newJobValidate($customer_id, $location_from, $location_to, $location_bill);
        if (count($errors)) {
            throw ValidationException::withMessages($errors);
        }

        $job = new \App\Models\Customer\Job();
        $job-&gt;customer_id = $customer_id;
        $job-&gt;location_id_from = $location_from-&gt;id;
        $job-&gt;location_id_to = $location_to-&gt;id;
        $job-&gt;location_id_bill = $location_bill-&gt;id;
        $job-&gt;save();

        return $job;
    }

    private function newJobValidate(int $customer_id, Location $location_from, Location $location_to, Location $location_bill)
    {
        $errors = [];

        if ($location_to-&gt;type === Location::TYPE_COMMON &amp;&amp; $location_from-&gt;type === Location::TYPE_COMMON) {
            // same location, but not customer's
            $errors[] = __('job.errors.same_location_common');
        }

        if ($location_to-&gt;type === Location::TYPE_CUSTOMER &amp;&amp; $location_to-&gt;customer-&gt;customer_id !== $customer_id ||
            $location_from-&gt;type === Location::TYPE_CUSTOMER &amp;&amp; $location_from-&gt;customer-&gt;customer_id !== $customer_id) {
            $errors[] = __('job.errors.location_doesnt_belong_to_customer');
        }

        if ($location_bill-&gt;id === null) {
            if ($location_to-&gt;type === Location::TYPE_CUSTOMER &amp;&amp; $location_from-&gt;type === Location::TYPE_CUSTOMER) {
               $errors[] = __('job.errors.must_select_bill_location');
            } elseif ($location_from-&gt;type === Location::TYPE_CUSTOMER) {
                $location_bill-&gt;id = $location_from-&gt;id;
            } else {
                $location_bill-&gt;id = $location_to-&gt;id;
            }
        }

        if ($location_bill-&gt;id !== $location_from-&gt;id &amp;&amp; $location_bill-&gt;id !== $location_to-&gt;id) {
            $errors[] = __('job.errors.bill_location_different');
        }

        return $errors;
    }
}
```
An important question: why don’t you want to touch the database in the test? You have a few different options for getting phpunit to play nicely with a database. Doing so lets you test things more thoroughly as you can do assertions that the data has landed in the database as you expected. 

As a starting point, based on what I see in this method, I would recommend adding something along these lines in phpunit.xml within the &lt;php&gt; tag

 &lt;env name="DB_DATABASE" value="testing"/&gt;


And create a database lined up with the “testing” value set in DB_DATABASE. These values equate to .env values, so you can add additional DB_USERNAME, etc values as appropriate. 

You can use the RefreshDatabase trait to reset migrations within your newly created testing database on each test. That is described at https://laravel.com/docs/6.x/database-testing#resetting-the-database-after-each-test 

It looks like you need some Location and Customer data in your database for this job to work. If you don’t already have factories for these models, make some as described at https://laravel.com/docs/6.x/database-testing#writing-factories 

As a starting point you can manually call factories in your test – factory(App\Customer::class)-&gt;create(); will put a factory generated customer into your database. If you need three of them, – factory(App\Customer::class, 3)-&gt;create(); The factory method returns the created entities, so you can do: 

    $customer = factory(App\Customer::class)-&gt;create();
    $locations = factory(App\Location::class, 3)-&gt;create();
    newJob(
     $customer-&gt;id, 
     Arr::get($locations, 0)-&gt;id, 
     Arr::get($locations, 1)-&gt;id, 
     Arr::get($locations, 2)-&gt;id
);


Now the job should be in your testing.jobs table, and you can make an assertion of that:

$this-&gt;assertDatabaseHas(‘jobs’,[
‘customer_id’ =&gt; $customer-&gt;id,
‘location_id_from’ =&gt; Arr::get($locations, 0)-&gt;id,
‘location_id_to’ =&gt; Arr::get($locations, 1)-&gt;id,
‘location_id_ bill’ =&gt; Arr::get($locations, 2)-&gt;id,
]);


Yes there’s a some overhead in getting the database incorporated into your test setup and in writing the factory, but the end result is a more thorough test. Worth it, in my opinion. 

I hope something here helps!