Showing posts with label unit testing. Show all posts
Showing posts with label unit testing. Show all posts

Thursday, February 28, 2013

A decent article on what tends to upset me about rails

From http://collectiveidea.com/blog/archives/2012/06/28/wheres-your-business-logic/

 I put some of the blame on Rails itself, which has guided developers to use Controllers, Models, or Libraries, and nothing else. 
and
 Are your tests slow (>1s to run a single unit test)? Do your tests have large ungainly setup? Are you using factory_girl in your unit tests? Are you mocking implementation instead of interfaces? 

I'm only really into my 3rd or 4th rails application of appreciable size, and they all seem to have this kind of problem - the tight coupling of active record to the model, and the model mixing persistence with business logic is rampart.

Enter the Interactor. An Interactor handles a use case. It pulls together the models and libraries it needs to process a single business rule, and then it’s done. These objects are very easy to test and use and in proper OO fashion can be used anywhere the app needs to apply the use case or business rule.
I dont think you need to call it an Interactor, it's just a Model. Not the relational-persistance kind, but the Model of your business logic - expressly applying SRP.

Enhanced by Zemanta

Friday, July 20, 2012

Run all tests related to a method you just refactored

$ sudo apt-get install ack-grep

For Ruby:
$ ack-grep "method_foo_bar" -l spec | xargs script/spec

For PHP
$ ack-grep "method_foo_bar" -l tests | xargs phpunit

Friday, April 24, 2009

Handy hint for unit tests

We've got loads of unit tests. A run takes approximately 20 minutes.

This is because we've got a lot of database interaction, and the re-engineering effort required to go back and mock that all out is immense.

So, what's the best way to make sure you catch problems quickly?

In your AllTests.php, make it a policy to put new test suites at the top, rather than the bottom.


Basically:
    public static function suite() {
$suite = new PHPUnit_Framework_TestSuite();

$suite-<addTestSuite('NewTest');
$suite-<addTestSuite('CoreTest');

return $suite;
}


This is the opposite thinking than "write your most important test cases first", but it helps you find the newest broken features.

Reblog this post [with Zemanta]

Friday, December 19, 2008

WTF: Refactoring snippet of the day

<?php
class SortingClassNameHere {
public function __construct($string) {
$this->string = $string;
}

private static $c = null;

public static function cmp($a, $b) {
$r = strnatcasecmp($a[self::$c], $b[self::$c]);
return ($r > 0 ? 1 : ($r < 0 ? -1 : 0));
}

public function process($data) {
self::$c = $this->string;
if (!usort($data, array("SortingClassNameHere", "cmp"))) {
throw new Exception('Unable to sort results.');
}

return $data;
}
}


Hints:
* SortingClassNameHere::cmp() is never called anywhere else in the code base apart from process()
* If you don't know why this is bad, I will shoot you.

Reblog this post [with Zemanta]

Wednesday, December 10, 2008

PEAR Bug Triage Day Results (December 6th)

PEAR's December Bug Triage Day was alright, if somewhat quiet. It was actually more spread out over the preceding weeks rather than much on the day itself.

Christian got a new version of Services_Blogging out earlier in the month, while Console_GetArgs and Numbers_Words got updated and increased unit test coverage - just about everyone under the sun chipped in and wrote translated unit tests for Numbers_Words - Igor, Christian, Lorenzo, Kouber, David and anyone else I missed.

Payment_DTA got a new owner in Martin Schutte, which saw a good few bug fixes applied.

I fixed up Text_Figlet, which broke PHP 4 compatibility, and got out a release of it and Services_Yadis.

Finally, Validate got back in the unit test good books, with almost all unit test failures resolved.

The next one is calendared for December 28th, so we'll see how that goes :)



Reblog this post [with Zemanta]

Thursday, November 13, 2008

PEAR and Unit tests - HTTP_Request2

Alexey is chugging away putting together HTTP_Request2, a PHP5 friendly next version of HTTP_Request.

When you look the previous package, HTTP_Request, it's pretty widely used. Within PEAR, these packages make use of it:
* Cache
* File_Sitemap
* HTTP_Client
* HTTP_WebDAV_Client
* Net_CDDB
* Net_DNSBL
* Net_Monitor
* Net_SMS
* pearweb
* Services_Amazon
* Services_Amazon_S3
* Services_Compete
* Services_Delicious
* Services_DynDNS
* Services_ExchangeRates
* Services_Hatena
* Services_OpenSearch
* Services_Pingback
* Services_Technorati
* Services_Trackback (versions <= 0.4.0)
* Services_TwitPic
* Services_W3C_CSSValidator
* Services_W3C_HTMLValidator
* Services_Weather
* Services_Yadis
* Services_Yahoo
* SOAP
* Testing_Selenium (versions <= 0.2.1)
* XML_GRDDL

Outside of PEAR; I'm sure there are lots more direct uses of HTTP_Request.

I know it's a valuable part of my code kung-fu kit and I love its API simplicity. It just works. Works well, too.


So HTTP_Request2 is the next iteration of the package - it features the notion of Adapters, so you can use the HTTP library of your choice - be it CURL, be it raw sockets, or for the more esoteric, TCP/Pidgeon.

The other great bit to this is the Mock Adapter. That's right ladies and gents; unit testing just got a whole lot better - just inject an instance of HTTP_Request2 with the Mock Adapter in, and have your tests run in mere moments!

A Quick Example


Observe how you might choose to execute all possible paths in this googlez() function.

<?php
require_once 'HTTP/Request2.php';
require_once 'HTTP/Request2/Adapter/Mock.php';

function googlez(HTTP_Request2 $client) {
$response = $client->send();

switch ($response->getStatus()) {
case '400':
print "YAY; we gots a response, we can haz googlez\n";
return true;
break;
case '403':
print "NAOOOOOO googlez forbidden\n";
return false;
break;
}
}


$mock = new HTTP_Request2_Adapter_Mock();
$mock->addResponse(new HTTP_Request2_Response('HTTP/1.1 400 Success'));
$mock->addResponse(new HTTP_Request2_Response('HTTP/1.1 403 Forbidden'));

$client = new HTTP_Request2('http://google.com/');
$client->setAdapter($mock);


// I can has googlez?
assert(googlez($client) === true);

// LOLWUT; I shouldz find out wha happenz if google explodez
// I fink it willz give me false.
assert(googlez($client) === false);


If you are doing it right, you will get:

YAY; we gots a response, we can haz googlez
NAOOOOOO googlez forbidden

Swapping this over to PHPUnit would be trivial.

Keep your ears open for the upcoming HTTP_Request2 release, and I would strongly encourage people to adopt it for their existing packages :)

Wednesday, November 05, 2008

PEAR Unit test results via Twitter

I just set up a few cron jobs to publish PEAR Unit test results, on the web, and via Twitter / Identi.ca / Atom feed, each and every night.

Go take a look!

Tuesday, July 29, 2008

PHPUnit Custom Assertations

Raphael has a nice quick article on custom assertions in PHPUnit.

Worth a read, it's blindingly obvious and simple, but the most clever things always are.

Even better, there's a link to xunitpatterns.com.

Wednesday, August 29, 2007

CoScripter, Selenium, Repetitive Testing and IBM

CoScripter (formerly Koala) is an automated testing tool for web applications.

Don't waste time reading further, just watch this awesome demo.

This neatly fits into the testing tools we've got at work: a continuous integration box, phpunit constantly running, phpcodesniffer watching for dangerous code, zend code analyzer doing the same, and our planned implementation of Selenium+PHPUnit.

Now we'll be able to tag team with Selenium and this tool.

Thank you, Mike & IBM, life just got easier.

Wednesday, July 04, 2007

Making Useful PHPUnit results

So you have about eleven billion test results, and you need a pretty way to quickly see failures.
What you need is a continous integration environment, and if you don't want to have to stick things into a database, I have the answer for you.

PHPUnit 3.0.6 provides (the first working) version of test-results as JSON.
Basically, you run:
phpunit --log-json results.js MyTest.php


And out comes JSON.

The next step is pretty simple - you just need a bit of Prototype magic, some css, and some love.
What you end up with is:
Our Tests

Here's the javascript, php, and css behind it. I'll leave it up to you to find nice icons.

<?php
$file = dirname(__FILE__) . '/results.js';
if (!file_exists($file)) {
die("No unit tests have been run?");
}
$json = file_get_contents($file);

//Ugh
$json = '[' . str_replace("}{", "},{", $json) . ']';

?>
<html>
<head>
<script src="/prototype.js"></script>
</head>
<body>
<style>
table {
border: 1px solid rgb(240, 240, 240);
}

.error {
border: 1px solid orange;
}

.fail {
border: 1px solid red;
}

.pass {
border: 1px solid green;
}

.cell {
width: 200px !important;
text-align: center;
padding: 0.5em;
}
.cell img {
display: block;
margin: auto;
}

h4 {
font-weight: normal !important;
font-style: italic;
}
</style>
<script type="text/javascript">
var total_passes = 0;
var total_failures = 0;
var total_errors = 0;
var total_skipped = 0;
var total_incomplete= 0;

function renderSuiteStart(item) {
var h2 = document.createElement("H2");
h2.appendChild(document.createTextNode(item.suite));

var p = document.createElement("p");
p.appendChild(document.createTextNode(item.tests + " tests in suite"));

var div = document.createElement("div");
var table = document.createElement("table");

div.id = item.suite;
table.id = item.suite + "_table";
table.className = '';

div.appendChild(h2);
div.appendChild(p);
div.appendChild(table);

$('main').appendChild(div);
}

function renderTest(item) {
var h3 = document.createElement("H3");
var h4 = document.createElement("H4");
h3.appendChild(document.createTextNode(item.test));

var p = document.createElement("p");
p.appendChild(document.createTextNode(item.time + " tests in suite"));

var tr = document.createElement("TR");
var th = document.createElement("TH");
var message = document.createElement("TD");
var detail = document.createElement("TD");
var time = document.createElement("TD");
var img = document.createElement("IMG");
var backtrace = document.createElement("OL");


time.appendChild(document.createTextNode(item.time));


img.alt = item.status;
message.className = item.status + " cell";

switch (item.status) {
case 'error':
if (item.message == "Skipped Test") {
message.className = "skipped cell";
total_skipped++;
} else if (item.message == "Incomplete Test") {
message.className = "incomplete cell";
total_incomplete++;
} else {
total_errors++;
}
img.src = '/presentation/common/img/emblem-important-small.gif';
break;
case 'fail':
total_failures++;
img.src = '/presentation/common/img/cross.gif';
break;
default:
total_passes++;
img.src = '/presentation/common/img/check.gif';
break;
}

item.trace.each(
function(trace) {
var li = document.createElement("LI");
var text = trace.file + "(line " + trace.line + "): " + trace.class + trace.type + trace.function + "()";
li.appendChild(document.createTextNode(text));

backtrace.appendChild(li);
}
);

h4.appendChild(document.createTextNode(item.test));
detail.appendChild(h4);
detail.appendChild(backtrace);

message.appendChild(img);
message.appendChild(document.createTextNode(item.message));
message.style.width = "300px";

tr.appendChild(message);
tr.appendChild(detail);


$(item.suite + "_table").appendChild(tr);
}

function renderResults() {
var data = $A(<?php print $json; ?>);

data.each(function(item) {
switch (item.event) {
case 'suiteStart':
renderSuiteStart(item);
break;
case 'test':
renderTest(item);
}
});

renderTOC();
}

function renderTOC() {
var fail = $A(document.getElementsByClassName('fail'));
var pass = $A(document.getElementsByClassName('pass'));
var error= $A(document.getElementsByClassName('error'));
var skipped= $A(document.getElementsByClassName('skipped'));
var incomplete= $A(document.getElementsByClassName('incomplete'));

var menu = document.createElement('p');
var checkbox_pass = document.createElement('input');
var checkbox_fail = document.createElement('input');
var checkbox_error = document.createElement('input');
var checkbox_skipped = document.createElement('input');
var checkbox_incomplete = document.createElement('input');

var label_pass = document.createElement('label');
var label_fail = document.createElement('label');
var label_error = document.createElement('label');
var label_skipped = document.createElement('label');
var label_incomplete = document.createElement('label');
var status_image;

menu.style.position = 'fixed';
menu.style.bottom = 0;
menu.style.right = "1.5em";
menu.style.backgroundColor = 'rgb(240, 240, 240)';
menu.style.padding = "1em;";
menu.style.fontWeight = 'bolder';

checkbox_pass.type = "checkbox";
checkbox_fail.type = "checkbox";
checkbox_error.type = "checkbox";
checkbox_skipped.type = "checkbox";
checkbox_incomplete.type = "checkbox";

checkbox_pass.checked = "checked";
checkbox_fail.checked = "checked";
checkbox_error.checked = "checked";
checkbox_skipped.checked = "checked";
checkbox_incomplete.checked = "checked";


checkbox_pass.onclick=function(){toggle(pass)};
checkbox_fail.onclick=function(){toggle(fail)};
checkbox_error.onclick=function(){toggle(error)};
checkbox_skipped.onclick=function(){toggle(skipped)};
checkbox_incomplete.onclick=function(){toggle(incomplete)};

label_pass.appendChild(checkbox_pass);
label_fail.appendChild(checkbox_fail);
label_error.appendChild(checkbox_error);
label_skipped.appendChild(checkbox_skipped);
label_incomplete.appendChild(checkbox_incomplete);

label_pass.appendChild(document.createTextNode("Passed (" + total_passes + ")"));
label_fail.appendChild(document.createTextNode("Failed (" + total_failures + ")"));
label_error.appendChild(document.createTextNode("Error (" + total_errors + ")"));
label_skipped.appendChild(document.createTextNode("Skipped (" + total_skipped + ")"));
label_incomplete.appendChild(document.createTextNode("Incomplete (" + total_incomplete + ")"));

label_pass.style.color = "green";
label_fail.style.color = "red";
label_error.style.color = "#FF6600";
label_skipped.style.color = "blue";
label_incomplete.style.color = "gray";

status_image = document.createElement("IMG");
status_image.style.display = "block";
status_image.style.margin = "auto";
if (total_failures > 0) {
status_image.src = "/presentation/common/img/emblem-unreadable.png";
} else {
status_image.src = "/presentation/common/img/compliance.png";
}
menu.appendChild(status_image);

menu.appendChild(label_pass);
menu.appendChild(label_fail);
menu.appendChild(label_error);
menu.appendChild(label_skipped);
menu.appendChild(label_incomplete);

checkbox_pass.onclick();
checkbox_pass.checked = "";

checkbox_skipped.onclick();
checkbox_skipped.checked = "";

checkbox_incomplete.onclick();
checkbox_incomplete.checked = "";

document.body.appendChild(menu);
}

function toggle(type) {
type.each(function(td) {
if (td.parentNode.style.display != 'none') {
td.parentNode.style.display = 'none';
} else {
td.parentNode.style.display = 'table-row';
}
});
}

function hide(type) {
type.each(function(td) {
td.parentNode.style.display = 'none';
});
}

function show(type) {
type.each(function(td) {
td.parentNode.style.display = 'table-row';
});
}

window.onload = renderResults;
</script>
<div id="main" class="contentbox">
<h1>Unit tests</h1>
<p><a href="results.txt">Raw results</a> | <a href="docs.html">Agile Documentation</a> | <a href="recent.php">Recent changes</a> | Generated: <?php print date("F j, Y, g:i a", filemtime($file)); ?> </p>
</div>
</body>
</html>

Saturday, May 26, 2007

assertSame() & phpunit

Take a look at enhancing assertSame() error messages for phpunit.

This is one of my most frequent gotchas, usually happening about once a fortnight. It drives me insane.

Basically, I do:

$expected = "Catfish";
$actual = SomeComplicatedConvolutedClass::method(); //returns "Catfish"
$this->assertSame($excepted, $actual);


and get an error message telling me that null isn't the same as Catfish. That's perfectly good, except I haven't clicked that the bug is my typo, not in SomeComplicatedConvolutedClass::method()

What a pain!

Wednesday, May 23, 2007

Mock Database objects, in pearweb

Some things to get you started. pearweb is a pear installable version of the pear website and package management tools.

It would be fair to say it's a little bit... old. However, there are signs of life in the old beast yet, with a slow but sure unit testing suite being built up.

I'm a phpunit junkie, but I haven't used half of the features in it. As betrand points out, there's a lot of complexity under the hood of phpunit.

It's probably for that reason that pearweb have rolled their own very simple unit testing framework. It provides all of your basics; assertTrue, assertFileExists, and what have you. It's called PEAR_PHPTest.

That isn't what I want to talk about, though. I want to talk about the implementation of mock database objects in pearweb, for unit testing purposes.

So: a unit test isn't a unit test if it relies on a database. At work, we usually don't care, and just expect a database to be there. pearweb is a little bit more strict.

So pearweb uses PEAR::DB, and to implement a mock database object, they have created a mock db driver.

To use it is pretty simple, if a little tedious. I'm sure some kind of code generator helper would save heaps of time here.

All you do is:
$mock->addDataQuery("SELECT * FROM categories ORDER BY name",
array(array('id' => 1,
'parent' => null,
'name' => 'test',
'summary' => null,
'description' => 'hi there',
'npackages' => 0,
'pkg_left' => 0,
'pkg_right' => 0,
'cat_left' => 1,
'cat_right' => 2)),
array('id', 'parent', 'name', 'summary', 'description', 'npackages', 'pkg_left',
'pkg_right', 'cat_left', 'cat_right'));


The first parameter is obviously the sql you are executing, the second is an array of result arrays, the third is the columns you expect in the result.

This means you can freeze the exact state of an object in the database, and run unit tests against it, without having to hit the database. Say bye bye to slow tests!


There are methods to simulate updating, inserting, and deleting too.

$sql = "INSERT INTO notes (id,uid,nby,ntime,note) VALUES(%s,%s,'%s','%s','%s')";
$sql = sprintf($sql, $data['id'], $data['uid'], $data['nby'], $data['ntime'], $data['note']);
$mock->addInsertQuery($sql, array(), 1);


So what, you say. That's pretty pointless, you say. Well, not if you expect a sequence of database queries to happen, and happen exactly how you want them to.

For that, we have the queries property of a mock db object.


$db = DB::factory('mock');

$myObject = new MyObject();
$myObject->removeLosers();

$phpunit->assertTrue($db->queries ==
array("SELECT id FROM losers",
"DELETE FROM users");


With a little bit of help from a code generator, there's a definite potential to never be in the dark about what queries were executed again.

Neat, hey. In order to bring some of this goodness to the rest of you, I've logged #11107: Add a PEAR::DB driver for mock objects and #11108: add a PEAR::MDB2 driver for mock objects.

Even if neither of those bugs get implemented, you can always grab the code from CVS

Monday, April 09, 2007

Google Stubs

The Google Testing Blog points us to stubs; one of the things I never get right with unit tests.

To summarize:
  • Test Double is a generic term for any test object that replaces a production object.
  • Dummy objects are passed around but not actually used. They are usually fillers for parameter lists.
  • Fakes have working implementations, but take some shortcut (e.g., InMemoryDatabase).
  • Stubs provide canned answers to calls made during a test.
  • Mocks have expectations which form a specification of the calls they do and do not receive.
... which I rarely make use of.

Instead of actually connecting to a database and reading information, a stub just returns the results you'd expect, or simulates a failure; and so forth.

Behaviour Driven Development

Behaviour Driven Development swept in like a breathe of fresh air.

Basically, it's unit testing (ala JUnit), but at a higher level - it's also an implementation of capturing user stories.

For example:
Scenario 1: Account is in credit
Given the account is in credit
And the card is valid
And the dispenser contains cash
When the customer requests cash
Then ensure the account is debited
And ensure cash is dispensed
And ensure the card is returned

becomes
public class AccountIsInCredit implements Given {
public void setup(World world) {
…
}
}

public class CardIsValid implements Given {
public void setup(World world) {
…
}
}

public class CustomerRequestsCash implements Event {
public void occurIn(World world) {
…
}
}
... and so forth.

All of your assumptions then get instantiated, and evaluated, before passing off the results to the outcome handling bits.

Doesn't sound like much, does it, but I'm always running into problems like this.


public function testDoesntAllowDatesInThePast() {
$foo = $this->document;

try {
$foo->Date = "1900-01-01";

$xml = $foo->asXML();

$this->fail("SanityFailureException expected!");
} catch (SanityFailureException $sfe) {

}
}

public function testDoesntAllowDatesInTheFuture() {
//... The same expectations are checked, across 95 other tests as well.
}


With JBehave, I'd be able to piece together several different pieces of setup - make the objects, set values x,y,z; call some other function over there; and then inspect the results I get back at a much higher level of detail.
I could simply have a "does it validate against an XSD" outcome written, as well as two or three other fine grained ones.

Using Agile Documentation, you can make something somewhat comprehensible to business people; as well.

The above example would become
XMLDocument
- Doesnt allow dates in the past
- Doesnt allow dates in the future


Go and have a read of Dan North's blog for more about this