-
Notifications
You must be signed in to change notification settings - Fork 195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to mocking database trans_status() in controller #384
Comments
If you believe CodeIgniter's database library works fine, I recommend you use mock. See how to mock Mock the all methods you use in your controller, and inject the mock object into the controller |
I want to keep the DML (insert, edit delete) method, only mocking result of |
I don't recommend you use monkey patch. What do you want to test? CodeIgniter code? DBMS? or your app code? When you use the real db connection, you test CodeIgniter code and DBMS and your code.
|
I want to test how my application can react (catch) against database failure, in my controller I do many database operation such as inserts and updates using transaction. I want to make sure that if the transaction fails, it will through the else block, which is return to the form and showing the error message to user, that some error occurred, then they can submit the form again. |
Call $mock = $this->getMockBuilder(stdClass::class)
->setMethods(['set'])
->getMock(); See https://phpunit.readthedocs.io/en/8.5/test-doubles.html If you use PHPUnit 9, use |
You could use |
I replace the code bellow: //MonkeyPatch::patchMethod('CI_DB_driver', ['trans_status' => false]);
$this->request->addCallable(function ($CI) {
$db = $this->getMockBuilder('CI_DB_driver')->disableOriginalConstructor()->getMock();
$db->method('trans_status')->willReturn(false);
$CI->db = $db;
}); and result error |
Mock the real class you actually use. If you use $this->request->addCallable(function ($CI) {
$db = $this->getMockBuilder('CI_DB_mysqli_driver')->disableOriginalConstructor()->getMock();
$db->method('trans_status')->willReturn(false);
$CI->db = $db;
}); |
I already tried before, I'm using mysqli then result another error |
To what did you call |
I don't call any database operation from test, error come from the controller which I called from $this->request because the I mock the $db. The message lack of information, which line produce the error or where the null $db is. The stack trace pointing wrong line number of code, when I checked the line it is pointing the comment or the code that I never test. Ah i know where the line of code pointing not our source code but in |
It is because you use Monkey Patching. |
Why don't you run step execution and debug? |
the controller call method from model that selecting data from database public function inactive($id)
{
// this line is error in test, it says error call select() on null
// getById($id) contains $this->db->from('trainings')->select('trainings.*')->where(['id' => $id])->get()->row_array()
$training = $this->training->getById($id);
$this->db->trans_start();
$this->training->update([
'status' => TrainingModel::STATUS_INACTIVE,
], $id);
$this->statusHistory->create([
'type' => StatusHistoryModel::TYPE_TRAINING,
'id_reference' => $id,
'status' => TrainingModel::STATUS_INACTIVE,
'description' => $this->input->post('message'),
]);
$this->db->trans_complete();
// I want to mock the trans_status() result to false, so it will go the else block
if ($this->db->trans_status()) {
flash('warning', "Training {$training['curriculum_title']} for employee {$training['employee_name']} is successfully deactivated");
} else {
// I want to check this line
flash('danger', "Deactivate training failed, try again or contact administrator");
}
redirect('training/class');
} |
Why don't you mock Ah, you probably forget calling |
This test passed. <?php
/**
* @property CI_DB_pdo_sqlite_driver $db
*/
class Db_trans extends CI_Controller
{
public function __construct()
{
parent::__construct();
$this->load->database();
}
public function index()
{
$this->db->trans_start();
$this->db->trans_complete();
if ($this->db->trans_status()) {
echo 'Okay';
} else {
echo 'Error';
}
}
} <?php
class Db_trans_test extends TestCase
{
public function test_index()
{
$this->request->addCallable(function ($CI) {
$db = $this->getMockBuilder('CI_DB_pdo_sqlite_driver')
->disableOriginalConstructor()
->setMethods([
'trans_status',
'trans_start',
])
->getMock();
$db->method('trans_status')->willReturn(false);
$db->method('trans_start')->willReturn(true);
$CI->db = $db;
});
$output = $this->request('GET', 'db_trans');
$this->assertSame('Error', $output);
}
} |
setMethods() is deprecated but I already add setMethods()/onlyMethods() and the error still same. In my test using database transaction to rollback data after request, is this caused the error? public function setUp(): void
{
parent::setUp();
if ($this->databaseTransactions) {
$this->resetInstance();
$this->db = $this->CI->load->database('testing', TRUE);
$this->db->trans_begin();
$DB = $this->db;
$this->request->setCallablePreConstructor(
function () use ($DB) {
// Inject db object
load_class_instance('db', $DB);
}
);
}
}
public function tearDown(): void
{
if ($this->databaseTransactions) {
if (is_object($this->db->conn_id) || is_resource($this->db->conn_id)) {
$this->db->trans_rollback();
$this->enableTransaction(true);
}
}
parent::tearDown();
} |
You inject the real db object in your It is recommended to write tests that either access the DB or use mocks to not access the DB at all. It is possible to mock only a part of db access, but I don't think it would make much sense as it would only increase the complexity. |
Maybe no. The db object in the |
Use Any way, all you to do is to fix the db mock not to cause errors. If you get |
I use the example of your project ci-app-for-ci-phpunit-test, in other framework that I used to, they have capability to rollback out of the box, no need manually setup. In this one I just copy from the example code, is it wrong with that? I want to run integration testing so I need to write in real database.
Why I need to mock others method in DB object? when I try to mock model it's fine to mock single method only, and other method not affected by that. I'm sorry if I got confuse and frustrated to resolve this problem, I'm beginner at Software Testing, in many case I just test simple functionality of application such as validate result of API or something similar |
The test code is not wrong, because all the tests pass in the repository.
Because the mock you create can't access the database at all. I figured out what you exactly want to do. $db['test'] = array(
'dsn' => '',
'hostname' => 'localhost',
'username' => 'username',
'password' => 'password',
'database' => 'codeigniter',
'dbdriver' => 'mysqli',
'dbprefix' => '',
'pconnect' => FALSE,
'db_debug' => TRUE,
'cache_on' => FALSE,
'cachedir' => '',
'char_set' => 'utf8',
'dbcollat' => 'utf8_general_ci',
'swap_pre' => '',
'encrypt' => FALSE,
'compress' => FALSE,
'stricton' => FALSE,
'failover' => array(),
'save_queries' => TRUE
);
$params = $db['test'];
$this->request->addCallable(function ($CI) use ($params) {
$db = $this->getMockBuilder('CI_DB_mysqli_driver')
->setConstructorArgs([$params])
->setMethods([
'trans_status',
])
->getMock();
$db->method('trans_status')->willReturn(false);
$CI->db = $db;
}); If you get header error, add /**
* @runInSeparateProcess
*/
public function test_index() |
You can write like this with getDouble(): $db['test'] = array(
'dsn' => '',
'hostname' => 'localhost',
'username' => 'username',
'password' => 'password',
'database' => 'codeigniter',
'dbdriver' => 'mysqli',
'dbprefix' => '',
'pconnect' => FALSE,
'db_debug' => TRUE,
'cache_on' => FALSE,
'cachedir' => '',
'char_set' => 'utf8',
'dbcollat' => 'utf8_general_ci',
'swap_pre' => '',
'encrypt' => FALSE,
'compress' => FALSE,
'stricton' => FALSE,
'failover' => array(),
'save_queries' => TRUE
);
$params = $db['test'];
$this->request->addCallable(function ($CI) use ($params) {
$db = $this->getDouble(
'CI_DB_mysqli_driver',
[
'trans_status' => false,
],
[$params]
);
$CI->db = $db;
}); |
exactly, I just want to mock the trans_status() to return false, any functionality to database still same Ah I see, the mock object depends on configuration which passed from the constructor to making connection, $params = $db['test'];
$this->request->addCallable(function ($CI) use ($params) {
$db = $this->getMockBuilder('CI_DB_mysqli_driver')
->setConstructorArgs([$params])
->onlyMethods([
'trans_status',
])
->getMock();
$db->method('trans_status')->willReturn(false);
$db->initialize(); // I add this part
$CI->db = $db;
}); but test run longer and error again |
I found the solution but not totally perfect, due to mocked db that passed into the request is different object with db in test case that used for rollback the data state, and controller will not see changes of the test (created data or anything), so we need can use existing data to when run the test that available in both connection session. // $db in test case (used for rollback) is different with $db we mocked then
// we need use same data that exist before the test is running
$existingUser = $this->db->get_where('prv_users', ['id' => 1])->row_array();
$this->loginAs($existingUser, PERMISSION_SETTING_EDIT);
// this data will not available in the request
// $this->hasInDatabase('logs', []);
$this->request->addCallable(function ($CI) {
include(APPPATH . 'config/testing/database.php');
$params = $db['testing'] ?? [];
$db = $this->getMockBuilder('CI_DB_mysqli_driver')
->setConstructorArgs([$params])
->onlyMethods([
'trans_status',
'trans_complete',
])
->getMock();
$db->method('trans_status')->willReturn(false);
$db->method('trans_complete')->willReturnCallback(function () use ($db) {
$db->trans_rollback(); // rollback $db in the controller
});
$db->initialize();
// $db->insert('prv_users', []) // you can add transaction here that rollback in controller
$CI->db = $db; // all data changes of this test will not available in controller)
}); |
In my controller I'm using database transaction, I want to simulate when transaction is failed
in my test, I also implement transaction rollback
rollback on tearDown()
$this->db->trans_rollback();
how I can achieve this? I try mockBuilder not working
The text was updated successfully, but these errors were encountered: