jclee100's avatar

Rather new to testing, how do I test a method that contains $model->save()

I am trying to test a method that has a few possible outcomes. Successful outcomes result in the model being saved, can I write a unit test and test it without actually saving the model?

Here's the method, any help is appreciated.

class PollAndDownload {
    public function __construct(MySession $session)
    {
        $this->session = $session;
    }
    protected function syncAdGroup($operandAdGroup, $batchJobAdGroups)
    {
        $adGroupModels = AdGroup::where('adGroupId', $operandAdGroup->getId())->get();
        if (count($adGroupModels)) {

            // Update existing

            foreach ($adGroupModels as $adGroupModel) {
                $adGroupModel->syncBack($operandAdGroup);
                $adGroupModel->save(); // Success
            }

        } else {

            // Handle newly published

            foreach ($batchJobAdGroups as $adGroupModel) {
                if ($adGroupModel->name == $operandAdGroup->getName() &&
                    $adGroupModel->campaign->fieldsHelper()->id == $operandAdGroup->getCampaignId()
                ) {
                    $adGroupModel->syncBack($operandAdGroup);
                    $adGroupModel->save(); // Success
                }
            }

        }
    }
}
0 likes
18 replies
jclee100's avatar

Just to clarify I am asking how to write an unit test which does not require database.

I've been using dump/log to do debugging for this case.

jclee100's avatar

@EventFellows

Thanks. I know about testing with databases. Is there no way I can write the test which mocks the save without touching the db? Just need to know it got to that line.

ohffs's avatar

Assuming that AdGroup is an eloquent model - then you're going to be trying to swim upstream to avoid using any kind of database to test actual database methods were run on it. Using an in-memory sqlite db is way easier than trying to work around it. Or consider re-writing your code so it's easier to test the logic?

jclee100's avatar

@ohffs Yea, I can probably break it into 3 methods something like the following and just test the shouldSave() method. I am just not sure if it's the right way to do this. Is there a right way to do this?

Is it a valid trade-off for testability?

protected function syncAdGroup($operandAdGroup, $batchJobAdGroups) {
    $shouldSave = $this->shouldSave($operandAdGroup, $batchJobAdGroups);
    if($shouldSave !== false) {
        $this->actuallySave($shouldSave[0], $shouldSave[1]);
    }
}
protected function shouldSave($operandAdGroup, $batchJobAdGroups) {
    ...
    // some logic
    return [$targetedModels, $operandAdGroup];
    ...
    // or return false if should not save
    return false;
}
protected function actuallySave($adGroupModels, $operandAdGroup) {
    foreach($adGroupModels as $adGroupModel){
        $adGroupModel->syncBack($operandAdGroup);
        $adGroupModel->save(); // Success
    }
}
KamalKhan's avatar

This might work

  • Add a public property to your model.
class AdGroup extends Model
{
    public $fakeSave = false;
}
  • Create an observer for use in your tests.
class TestModelObserver
{
    public function saving($model)
    {
        $model->fakeSave = true;
        return false;
    }   
}
  • While testing, observe your model and check your assertions as follows:
// Arrange

// Somehow get the AdGroup model(s).
// Observe the model(s).
$adGroup->observe(TestModelObserver::class);

// Act

// Run your code and pass the model(s).

// Assert

// Assert that the model(s) was going to be saved in the database.
$this->assertTrue($adGroup->fakeSave);

// OR

// Assert that the model(s) was NOT going to be saved in the database.
$this->assertFalse($adGroup->fakeSave);
ohffs's avatar

@komirad your best option really is to just hit a db - an in-memory sqlite one is ideal if you don't want to hit the disk or set anything up. You don't need to worry about it having side effects or being drastically slow.

I guess I'm looking at your code and imaging you want to have a series of tests something like :

if there are existing adGroups with a given ID,
then when I call syncAdGroup with that ID,
the adGroups will be updated with whatever syncBack() does...

//
if there are no adGroups with an ID of 5,
then when I call syncAdGroup with an ID of 5,
... etc

So just testing that a ->save() method was called isn't very helpful. For instance, in a years time someone (maybe you) changes the code a little and now doesn't call ->save() but calls, say, ->update(). Now your test breaks even though the code is still achieving the correct goal.

I guess another way of thinking about it is - imagine you had those test descriptions - but didn't know the implementation of the syncAdGroup() function and had to write tests for them - then how would you do it? It's fairly unlikely you'd do it by mocking out an object and making sure a save() method was called? I think ;-)

It's Friday afternoon and I'm possibly rambling a bit though... ;-)

1 like
KamalKhan's avatar

@ohffs The saving model event (as per my solution) will be called in any case whenever the model is being sent to the db.

lara25260's avatar

The correct way is as follows.

Use mockery to mock classes.

Its built into laravel and documentation.

Your welcome.

KamalKhan's avatar

If you only mock what is yours, you mean to say you test Eloquent because it is not yours? I don't think many would agree to that. You mock Eloquent because it is already tested. Here PollAndDownload is not tested so you should not mock it. Once you unit test it, you may mock it when testing some other class that uses it because at this point, PollAndDownload is already backed by tests.

KamalKhan's avatar

Thats integration... You should test all your integrations... This topic is about unit tests.

usman's avatar

Thats integration... You should test all your integrations..

Yeah this is what you do when a database or an external resource is involved. There is no good way to mock eloquent because it uses a lot of static methods to do what it does.

lara25260's avatar

You don't use eloquent, eloquent is not mock able, eloquent is 100% tested with laravel.

Don't talk about good standards if your not even using repositories.

$repo = mock (repo)->has (update)->has (save)->return (collection)

Inject the interface to the controller. Since you have amazing standards as stated.

Now you call the method with your mocked object

new Method (Repo $repo) $repo->update(): $repo->save ():

Just a brief, I'm on a phone. Read documentation.

Please or to participate in this conversation.