I decided to create CreateClassroomService to separte logic in my controller method.
class CreateClassroomService extends Service
{
public function create(string $name, User $user): ?Classroom
{
$this->checkName($name);
$classroom = new Classroom();
$created = $classroom->setName($name)
->associateUser($user)
->save();
return $created ? $classroom : null;
}
private function checkName(string $name): void
{
if (empty($name)) {
throw new InvalidArgumentException();
}
}
}
I am trying to test this service as part of learning unit testing, but I don't know how. I don't know how to mock the classroom object to control what the method should return. Does this mean that creating this service was not a good idea because I am not able to test it? Should I build this service differently? Unless the service can be tested, but I don't know how... What should I check in assertion?
This is my test but it is not good because I am not able to force what should be returned.
public function testGivenCreateCorrectDataClassroomWillBeCreated(): void
{
$name = 'Test classroom';
$user = Mockery::mock(User::class);
$result = $this->service->create($name, $user);
$this->assertTrue($result);
}
CodePudding user response:
For something like this, you could simply assert that the ClassRoom has in fact been created. Docs
As per the docs, update your test class so that it's using the RefreshDatabase
trait e.g.:
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
Laravel has Model Factories to make creating models with dummy data very quick and easy. There should already be a UserFactory
created for you (you may need to update it if you've updated the default users migration).
public function testGivenCreateCorrectDataClassroomWillBeCreated(): void
{
$name = 'Test classroom';
$user = User::factory()->create();
$result = $this->service->create($name, $user);
$this->assertInstanceOf(ClassRoom::class, $result);
$this->assertDatabaseHas('class_rooms', ['name' => $name]);
}
Don't forget to import the User
and ClassRoom
models in to your test class.
CodePudding user response:
Not sure how mocking works in Mockery, but lets review what you are doing: your create function that you are trying to test creates a new instance of a Classroom
, and since it instantiates the object inline you have no control over it.
From the code you have written, I am assuming that you build a classroom object and return, never using the same creator instance again, so what you can do is add a constructor to CreateClassroomService
through which you inject a Classroom
object. If you are using the same create service to create multiple classroom objects at a time, you will also need to make sure that you somehow reset the classroom instance to its default state inbetween creation(s), or you inject a fresh & new classroom object before invoking create
again. This wholly depends on what the classroom does though.
The classroom object then can be mocked through unit testing -- you inject the mock instance and youre good to go. You actually are already injecting User
object, so you're already on the right lines!
class CreateClassroomService extends Service {
private ?Classroom $classroom;
public function __construct(Classroom $classroom) {
$this->classroom = $classroom;
}
...
}
Now you just need to mock your classroom object in your test, inject the mock into your service when instantiating the object, and off you go. :)
Btw I would also say you may want to consider do some more abstraction on your service in the form of interfaces or review your parent class to make it more unit testable in general. Generally speaking for effective unit testing you want to avoid static methods and new
keywords; where you can absolutely not avoid it, one approach might be to wrap just that one line of code in an encapsulated method, so you can mock the method instead to return you mock data / mock instances instead (but generally if you have to do this it should be an obvious sign to alert you to having sub-par architecture).