Reputation: 1144
I have a class Receipt.php
<?php
namespace TDD;
class Receipt {
private $user_id = 1;
private $pending_amount;
public function total(array $items = []){
$items[] = $this->pending();
return array_sum($items);
}
public function tax($amount,$tax){
return $amount * $tax;
}
private function pending()
{
$sql = 'select pending_amount from Pending_transtions where user_id =' . $this->user_id . ' limit 1;';
//$pending_amt = $this->mainDb->get_sql_row($sql);
//$this->pending = $pending_amt['pending_amount'];
return $this->pending_amount = 45;
}
public function addTaxPending($tax){
return $this->pending_amount * $tax;
}
}?>
And in my PHPUnit file, ReceiptTest.php
<?php
namespace TDD\Test;
require(dirname(dirname(__FILE__))).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php';
use PHPUnit\Framework\TestCase;
use TDD\Receipt;
class ReceiptTest extends TestCase{
public function setUp(): void {
$this->Receipt = new Receipt();
}
public function tearDown(): void{
unset($this->Receipt);
}
public function testTotal(){
$input = [0,2,5,8];
$output = $this->Receipt->total($input);
$this->assertEquals(15,$output,"this is not valid");
}
public function testTax(){
$inputAmount = 10.00;
$inputTax = 0.10;
$output = $this->Receipt->tax($inputAmount,$inputTax);
$this->assertEquals(1.0,$output,"this tax expecting 1.0");
}
}?>
question:
How to ignore internal calling function pending()
because it fetches data from DB. At the same time I want to access the property of $this->pending_amount
.
Here Pending()
must be private function.
How can I achieve that? I am looking for your valuable solutions
Upvotes: 3
Views: 714
Reputation: 6371
You need to mock the dependency. The dependency in your case is the database. You need to replace access to the database with a mock that you can configure with known return values.
Take the time to read the PHPUnit documentation on how to use mock objects.
Given a DB access class with a get_sql_row
method that accepts an SQL string and returns some data structure, we don't need to fill in any code here in our example because we're going to mock the get_sql_row
method and configure it to return known values for our test.
class MainDb
{
public function get_sql_row(string $sql)
{
// ...
}
}
Our receipt class accepts a DB access object in the constructor and uses it to execute SQL e.g. the pending
method executes SQL to get the pending amount (if any).
class Receipt
{
public function __construct($mainDb)
{
$this->mainDb = $mainDb;
}
public function total(array $items = [])
{
$items[] = $this->pending();
return array_sum($items);
}
private function pending()
{
$pendingAmount = $this->mainDb->get_sql_row('sql...');
return $pendingAmount['pending_amount'];
}
}
In the testcase setup method we create a mock of the DB access class and create the receipt object with the mocked database. The mock is then used in the tests to configure expectations, received arguments, and return values.
The testTotalWithNoPendingAmount
test configures the DB mock to return 0 when get_sql_row
is invoked. The get_sql_row
is only expected to be called once and will fail otherwise. Configuring get_sql_row
to return a known value allows us to test the behaviour of the receipt class without touching the DB.
Similarly, testTotalWithPendingAmount
configures the DB mock get_sql_row
to return a value other than 0 so that we can test behaviour of calculating the total when there is a pending amount.
final class ReceiptTest extends TestCase
{
protected Receipt $receipt;
protected function setUp(): void
{
// create a mock of the database access object
$this->dbMock = $this->createMock(MainDb::class);
// create receipt with mocked database
$this->receipt = new Receipt($this->dbMock);
}
public function testTotalWithNoPendingAmount(): void
{
$this->dbMock->expects($this->once())
->method('get_sql_row')
->willReturn(['pending_amount' => 0]);
$this->assertEquals(15, $this->receipt->total([0, 2, 5, 8]));
}
public function testTotalWithPendingAmount(): void
{
$this->dbMock->expects($this->once())
->method('get_sql_row')
->willReturn(['pending_amount' => 3]);
$this->assertEquals(18, $this->receipt->total([0, 2, 5, 8]));
}
}
Take the time to read the PHPUnit documentation on using mock objects and understand how they work and how to use them in your testing.
Note also that your handling of SQL is potentially open to SQL injection. Please read about SQL injection.
Upvotes: 2
Reputation: 3921
Proper solution is to replace your dependency (the one which is saved under $this->mainDb
in your example) with a "mocked" one in your test case.
Here is an article from PHPUnit manual, which shows how to create mocks - https://phpunit.readthedocs.io/en/9.5/test-doubles.html#mock-objects
--
Speaking about ways of injection: your can either pass $this->mainDb
instance via class constructor, or make so-called "seam" in form of public setMainDb
method (which is not an elegant solution - I'd prefer to avoid it).
Another thing which I had to do sometimes, is to replace the value via Reflection: make private property accessible and set it inside of test to the value I need.
--
Update:
Based on given example, I think the easiest way to achieve desired result is:
setUp
to: public function setUp(): void
{
$this->Receipt = new Receipt();
$mainDbMock = new class() {
public function get_sql_row() {
return [
"pending_amount" => 0
];
}
};
$this->Receipt->setMainDb($mainDbMock);
}
Receipt
class: public function setMainDb($mainDb)
{
$this->mainDb = $mainDb;
}
Upvotes: 2