Skip to content

Instantly share code, notes, and snippets.

@fideloper
Forked from anonymous/testable_user.php
Last active October 4, 2018 05:54
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fideloper/4394248 to your computer and use it in GitHub Desktop.
Save fideloper/4394248 to your computer and use it in GitHub Desktop.
An example of making a User class testable and maintainable, using dependency injection and some abstraction.
<?php
// Often, we see a class like this:
class User {
public function getCurrentUser()
{
$user_id = $_SESSION['user_id'];
$user = App::db->select('user')
->where('id', $user_id)
->limit(1)
->get();
if ( $user->num_results() > 0 )
{
return $user->row();
}
return false;
}
}
/*
1. The above isn't testable. Command-line won't have $_SESSION available
2. Above depends on specific database implementation
*/
// First, let's abstract this out to something more generic
class User {
public function getUser($user_id)
{
$user = App::db->select('user')
->where('id', $user_id)
->limit(1)
->get();
if ( $user->num_results() > 0 )
{
return $user->row();
}
return false;
}
}
/*
The above can now retreive any user
We've removed getting the user id from session data (a dependency) from this class
Let's improve this - we can still remove the amount of 'knowledge' the class needs
*/
// Next, let's start with some basic Dependency Injection
class User {
public function __construct($db_connection)
{
$this->_db = $db_connection;
}
public function getUser($userId)
{
$user = $this->_db->select('user')
->where('id', $userId)
->limit(1)
->get();
if ( $user->num_results() > 0 )
{
return $user->row();
}
return false;
}
}
/*
At this point, we're basically testable. We pass in the database connection and user id, so the class
needs no knowledge of its dependencies. We can run this in a unit test and test to see if we get results.
We can mock the $db_connect class, or actually pass in a real database object
Other than testability, at this point we can:
- switch out the database connection, for instance if we're switching from MySQL to PostgreSQL.
- pass in any user_id, instead of only getting the "current" user.
But we can still do better.
*/
// Add some further abstraction
interface UserDataInterface {
public function getUser($userId);
}
class MysqlUser implements UserDataInterface {
public function __construct($db_connection)
{
$this->_db = $db_connection;
}
public function getUser($userId)
{
$user = $this->_db->select('user')
->where('id', $userId)
->limit(1)
->get();
if ( $user->num_results() > 0 )
{
return $user->row();
}
return false;
}
}
class User {
public function __construct(UserDataInterface $userdata)
{
$this->_db = $userdata;
}
public function getUser($userId)
{
return $this->_db->getUser($userId);
}
}
// Usage
$mysqlUser = new MysqlUser( App:db->getConnection('mysql') );
$user = new User( $mysqlUser );
$userId = App::session('user_id');
$currentUser = $user->getUser($userId);
/*
So, what just happened?
1. We created an interface which will be used to get a user from our data source of choice
2. We created an implementation of the interface which happens to use MySQL
3. We enforce the use of a subclass of UserDataInterface, guaranteeing the getUser method is available
Results:
1. We keep testability thanks to DI
2. Enforcing the use of an interface allows us to switch the logic of how we get the user data freely.
We can use MySQL, other SQL, noSQL, arrays or any data source. For code
maintance, this means we can change out a whole storage engine and only change which class gets passed to our User class, as long
as it implements UserDataInterface.
3. Additionaly, the datasource can be fully mocked for testing.
*/
/*
That's cool for testing. However, when we code, we're left with some extra leg-work. Consider the above usage:
*/
$mysqlUser = new MysqlUser( App::db->getConnection('mysql') );
$user = new User( $mysqlUser );
$userId = App::session('user_id');
$currentUser = $user->getUser($userId);
/*
This is an annoying process - We need to perform all these lines of code for any user-related storage!
We can make this easier for ourselves using a Container
Example:
@link http://twittee.org/
@link http://pimple.sensiolabs.org/
*/
//Somewhere in a configuration file
$app = new Container();
$app['user'] = function() {
return new User( new MysqlUser( App::db->getConnection('mysql') ) );
}
/*
Now, anywhere in our code, we can get the User class with our MySQL connection.
*/
$currentUser = $app['user']->getUser( App::session('user_id') );
/*
This goes further toward making code maintainable - We can now change from MySQL
to another storage type in ONE location in our code!
*/
<?php
/*
An example unit test we can now perform
*/
use Mockery as m;
class UserTest extends PHPUnit_Framework_TestCase {
public function tearDown()
{
m::close();
}
public function testUserClass()
{
$user = new User( $this->getDbMock() );
$userId = 1;
$testUser = $user->getUser($userId);
$this->assertEquals($userId, $testUser->user_id);
}
protected function getDbMock()
{
// We're not testing the MySQL connection class
// We're testing that User class receives a user
$return = new stdClass();
$return->user_id = 1;
$return->first_name = 'Chris';
$return->last_name = 'Fidao';
$mock = m::mock('MysqlUser');
// Expect call to getUser method once, and return our $return object
$mock->shouldReceive('getUser')->times(1)->andReturn($return);
return $mock;
}
}
@fideloper
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment