Different methods of testing
First define what you are doing: Unit testing or integration testing. The number of layers is irrelevant for unit testing since you only test one class most likely. The rest you mock out. For integration testing it is inevitable that you test multiple layers. If you have good unit tests in place the trick is to make the integration tests not too complex.
If your unit tests are good you don't have to repeat testing all details when doing integration testing.
Terms we use, those are a bit platform dependent, but you can find them in almost all test / development platforms:
Example application
Depending on the technology you use the names may differ, but I will use this as an example:
If you have a simple CRUD application with model Product, ProductsController, and an index view which generates an HTML table with products:
The end result of the application is showing an HTML table with a list of all products which are active.
Unit testing
Model
The model you can test quite easily. There are different methods for it; we use fixtures. I think that's what you call "fake datasets". So before each test is run we create the table, and put in the original data. Most platforms have methods for this. For example, in your test class, a method setUp() which is run before each test.
Then we run our test, for example: testGetAllActive products.
So we test directly to a test database. We do not mock out the datasource; we make it always the same. This allows us for example to test with a new version of the database, and any query issues will come up.
In the real world you cannot always follow 100% single responsibility. If you want to do this even better you could use a datasource which you mock. For us (we use an ORM) that feels like testing already existing technology. Also the tests become much more complex, and they don't really test the queries. So we keep it this way.
The hard coded data is separately stored in the fixtures. So the fixture is like an SQL file with a create table statement and inserts for the records we use. We keep them small unless there is a real need to test with lots of records.
class ProductModel {
public function getAllActive() {
return $this->find('all', array('conditions' => array('active' => 1)));
}
}
Controller
The controller needs more work, because we don't want to test the model with it. So what we do is mock the model. That means: We test: index() method which should return a list of records.
So we mock the model method getAllActive() out and add fixed data in it (two records for example). Now we test the data the controller sends to the view, and we compare if we really get those two records back.
function testProductIndexLoggedIn() {
$this->setLoggedIn();
$this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
$result=$this->ProductsController->index();
$this->assertEquals(2, count($result['products']));
}
That's enough. We try to add as little functionality to the controller because that makes testing hard. But of course there is always some code in it. For example, we test requirements like: Show those two records only if you are logged in.
So, the controller needs one mock normally and a small piece of hardcoded data. For a login system maybe another one. In our test we have a helper method for it: setLoggedIn(). That makes it simple to test with login or without login.
class ProductsController {
public function index() {
if($this->loggedIn()) {
$this->set('products', $this->ProductModel->getAllActive());
}
}
}
Views
Views testing is hard. First we separate out logic which repeats. We put it in Helpers and test those classes strict. We expect always the same output. For example, generateHtmlTableFromArray().
Then we have some project specific views. We don't test those. It is not really desired to unit test those. We keep them for integration tests. Because we took out a lot of the code into views we have a lower risk here.
If you start testing those you likely need to change your tests every time you change a piece of HTML which is not useful for most projects.
echo $this->tableHelper->generateHtmlTableFromArray($products);
Integration testing
Depending on you platform here you can work with users stories, etc. It could be webbased like Selenium or other comparable solutions.
Generally we just load the database with the fixtures and assert which data should be available. For full integration testing we generally use very global requirements. So: Set product to active and then check if the product becomes available.
We don't test everything again, like whether the right fields are available. We test the bigger requirements here. Since we do not want to duplicate our tests from the controller or view. If something is really key / core part of you application or for security reasons (check password is NOT available) then we add them to ensure it's right.
The hard coded data is stored in the fixtures.
function testIntegrationProductIndexLoggedIn() {
$this->setLoggedIn();
$result=$this->request('products/index');
$expected='<table';
$this->assertContains($expected, $result);
// Some content from the fixture record
$expected='<td>Product 1 name</td>';
$this->assertContains($expected, $result);
}