Reputation: 1273
I have 3 classes.
class Box{
public $item1;
public $item2;
public function __construct($item1,$item2){
$this->item = $item1;
$this->item2 = $item2;
}
public function getItem1(){
return $this->item1;
}
}
class Details{
public $stuff
public $item1;
public $item2;
public $item3;
public function __construct($stuff){
$this->stuff = $stuff
}
public function setItem1($item){
$this->item1 = $item;
}
public function setItem2($item){
$this->item2 = $item;
}
}
class Crate{
public $box;
private $stuffString = "Stuff";
public function __construct(Box $box){
$this->box = $box;
}
public function getDetails(){
$details = new Details($stuffString);
$details->setItem1($box->item1);
$details->setItem2("Detail");
return $details;
}
}
The Crate->getDetails()
method returns a Details object with data from the Box object. I want to write tests for this method.
function test_get_details(){
$box = Mockery::mock(Box::class);
$box->shouldReceive('getItem1')->andReturn("BoxItem");
$crate= new Crate($box);
$details = $crate->getDetails();
$this->assertInstanceOf(Details::class,$details);
}
I create a mock of the Box class and pass it to constructor of Crate. When I call $crate->getDetails();
it should return a Details object with
I know I can test this by doing for each item $this->assertEquals("BoxItem",$details->item1);
etc... but is that the best way to go about it? Is there some PHPUnit tool to build up the desired Detials result and compare it
For Example
$this->assertEquals(MockDetailObject,$details)
or do I have to do a series of asserts to make sure the result is what I expect.
Note*
I know for my example this isn't a huge deal, I built it up quick to explain what I mean. But in the code I'm working on I ran into the same type of problem except the Details Object is more complex than just 3 strings.
Upvotes: 0
Views: 1854
Reputation: 76
TL;DR: create a factory and test this factory 100%.
From what I understood, your Crate
class is both an entity and a factory. You could refactor Crate::getDetails
by moving this creation responsibility to a factory.
This way you'll be able to unit test the creation logic only by using the "Given, When, Then" structure. Check out this post about clean tests and navigate to the "Tests should be concise and meaningful".
Having this structure will help you telling what are the inputs and outputs. For example:
CrateDetailsFactoryTest.php
class CrateDetailFactoryTest extends TestCase
{
public function testCreateCrateDetail(): void
{
// Given
$crate = $this->givenThereIsACrate();
$boxes = $this->givenThereAreTwoRedBoxes();
// When
$crateDetail = $this->crateDetailFactory->createCrateDetail(
$crate,
$boxes
);
// Then
// (Unnecessary instanceof, if you have strict return types)
self::assertInstanceOf(Detail::class, $crateDetail);
self::assertCount(2, $crateDetail->getBoxes());
self::assertEquals(
'red',
$crateDetail->getBoxes()->first()->getColor()
);
}
}
With this your creation logic is covered; From here you can simply inject your factory where you need, and during the unit test time you just mock it away:
CrateService.php
class CrateServiceTest extends TestCase
{
private $crateDetailFactory;
private $crateService;
public function setUp(): void
{
$this->crateDetailFactory = $this->prophesize(CrateDetailFactory::class);
$this->crateService = new CrateService(
$this->crateDetailFactory->reveal()
);
}
public function testAMethodThatNeedsCrateDetails(): void
{
// Given
$crate = $this->givenIHaveACrateWithTwoBoxesInIt();
$boxes = $crate->getBoxes();
// When
$result = $this->crateService->AMethodThatNeedsCrateDetails();
// Then
$this->crateDetailFactory->createCrateDetail($crate, $boxes)
->shouldBeCalledOnce();
}
}
I hope that was useful. Cheers! :)
Upvotes: 3
Reputation: 196
Using your classes above, to unit test this properly, you would have to use DI to inject \Details::class
into either getDetails()
or the __constructor. Then write tests for each method of each class, mocking any class dependencies/properties
class Create
{
public function getDetails(\Details $details)
}
//test.php
$mockDetails = $this->createMock(\Details::class)
->expects($this-once())
->method('item1')
->with('some_arg')
->willReturn('xyz')
$mockBox = $this-createMock(\Box::class)
......
$crate = new Create($boxMock);
$result = $crate->item1($mockDetails);
$this-assertSame('xyz', $result);
If it feels like your mocking way to much for one method, then you should consider refactoring to make the code more testable.
As far as assertions for multiple items, in PHPUnit you can use a dataprovider to pass an array of values as individual tests to one test method. PHPUnit Docs - Data Providers
You would also write separate unit tests for the \Details::class that asserted what is passed to \Details::setItem1($item) is actually set on the item1 property. Ie.
Testing \Details::class -
//test2
public function test() {
$details = new Details('some stuff');
$details->setItem1('expected');
self::assertSame('expected', $details->item1);
}
Upvotes: 1