1

I have a class that looks something like this:

class Foo {
    protected $_arr = array();

    public function has($key) {
        return array_key_exists($key, $this->_arr);
    }

    public function add($key, $val) {
        $this->_arr[$key] = $val;
    }
}

For my PHPUnit tests for these methods, the only way I can think if to test add() is by asserting that has() returns TRUE for the same key after adding it. This makes my testAdd() test dependent on my testHas() test. Conversely, the only way I can think of to test has() is basically doing the exact same steps, but this would make this test dependent on an already dependent test, producing a chicken and egg type problem.

Am I going about this the wrong way? What's a better method for testing stuff like this?

FtDRbwLXw6
  • 27,774
  • 13
  • 70
  • 107
  • I don't think this is a big deal. If one of them breaks - both return failed state. Either way you have to go and fix it. – Mikhail Feb 15 '12 at 17:35
  • BTW: `isset` is faster than `array_key_exists`. You should check whether this is applicable here (null-values) – TimWolla Feb 15 '12 at 17:48
  • possible duplicate of [Unit-testing a simple collection class](http://stackoverflow.com/questions/1195000/unit-testing-a-simple-collection-class) (language agnostic, not specfic to phpunit). – Gordon Feb 15 '12 at 18:31

4 Answers4

2

Instead of writing one-test-per-method, design your tests around the functionality the class must provide. You'll have tests that exercise multiple methods, but that's fine because it indicates to the developer that those methods are related. This is where Test Driven Development--where you write the tests while designing the contract for the class before writing the code for the class--really shines.

class FooTest extends PHPUnit_Framework_TestCase
{
    function testStartsEmpty() {
        $foo = new Foo;
        self::assertFalse($foo->has('bar'));
    }

    function testAddStoresKeys() {
        $foo = new Foo;
        $foo->add('bar', 'baz');
        self::assertTrue($foo->has('bar'));
    }

    function testAddStoresKeysWithNullValues() {
        $foo = new Foo;
        $foo->add('bar', null);
        self::assertTrue($foo->has('bar'));
    }

    function testAddStoresValues() {
        $foo = new Foo;
        $foo->add('bar', 'baz');
        self::assertEquals('baz', $foo->get('bar'));  // fatal error
    }
}

Now that testAddStoresValues() fails, it's time to implement a method to get the value for a given key: Foo::get($key).

David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • Couldn't agree more. Unit test is about test **behaviors** and **NOT** about testing single methods in isolation of the object context. You want to test what the object **does** to the outside world. How it works internally should be completely irrelevant for unit testing. – edorian Feb 15 '12 at 19:53
  • @DavidHarkness: When `testAddStoresValues()` fails, how does the developer know whether the root problem lies in adding or getting? This is a pretty trivial example, but in general, I try to limit the test to only testing one thing. This test has two points of potential failure, which makes debugging more difficult. +1 though for a good answer. – FtDRbwLXw6 Feb 15 '12 at 20:44
  • 3
    @drrcknlsn, the only other solution is to make your test more complicated by trying to poke values into the SUT, and I feel that's a worse situation because the test code doesn't have tests. Unit tests are designed detect failures and guide you to where the fix belongs. The smaller they are, the more localized the errors, but sometimes classes just aren't simple enough for every test to point to the exact failing line of code. – David Harkness Feb 15 '12 at 23:37
1

PHPUnit allows testing of non-public members.

However, using $sut->has() to find out whether $sut->add() worked is perfectly fine. Also, when you test $sut->add(), you dont need to write a separate test for $sut->has() as well, because it's covered in the $sut->add() test already. Just add a @covers annotation.

Gordon
  • 312,688
  • 75
  • 539
  • 559
  • Isn't the point of a unit test to only test a single "unit" at a time though? This would be testing both methods in one test, which I was staying away from because I thought that led to ambiguous failures and made debugging more difficult. – FtDRbwLXw6 Feb 15 '12 at 18:00
  • @drrcknlsn see http://stackoverflow.com/questions/1195000/unit-testing-a-simple-collection-class. actually, your question is somewhat of a duplicate of it. – Gordon Feb 15 '12 at 18:30
  • 1
    @drrcknlsn The **unit** in unit testing is the **object** not the method! You don't want to test methods in isolation from the other methods of the objects. (If you wanted to do so just use functions ;) ) - It's really important to test how the object behaves and if it does the expected think when called and not how the methods interact internally. – edorian Feb 15 '12 at 19:55
  • @edorian: That's not true. The "unit" in unit testing does not represent the object. That's somewhat of a weird notion, given that you can (and people do all the time) unit test procedural code (read: functions). The "unit" doesn't necessarily have to be a method; it could be a certain behavior within the method. But testing multiple methods is certainly going beyond testing a single unit. – FtDRbwLXw6 Feb 15 '12 at 20:35
  • 2
    Your `UnitTest` is a collection of `TestCases`. One test case tests one object behavior. All the test cases combined test one object. Writing one test per method is a testing anti pattern that hurts maintainability and use of a test suite. You shouldn't, for example, ever test setter but only test what happens after you called a view of them and then told the object to do something. – edorian Feb 15 '12 at 20:43
  • @edorian: I don't condone writing one test per method. In fact, it's often several tests per method in my case. I was saying that testing more than one method in a single test is bad practice, since you're supposed to be isolating the behavior being tested, as much as possible. Otherwise, we'd all be writing one giant test with dozens of asserts per class. :-p – FtDRbwLXw6 Feb 16 '12 at 01:05
  • 1
    @drrcknlsn of course you should not test add(), remove() and sort() all in one test, because they are separate behaviors. But using multiple methods pertaining to the same behavior is perfectly fine. And that's what everyone is trying to tell you. – Gordon Feb 16 '12 at 08:06
  • @Gordon: I'm finding it difficult to find where to draw the line between what is and is not "pertaining to the same behavior." In my particular example, the `add()` and `has()` are clearly somewhat related in that the result of one depends on the other, but we could say that about `add()` and `sort()`, as well, and it would seem to me that we shouldn't be using `add()` in the test(s) for `sort()`. – FtDRbwLXw6 Feb 16 '12 at 16:29
1

In reference to the long dicussions in the comments to @Gordon s answer:

The unit in unit testing is the object not the method! You don't want to test methods in isolation from the other methods of the objects. (If you wanted to do so just use functions ;) ) - It's really important to test how the object behaves and if it does the expected think when called and not how the methods interact internally.

I've recently written a blog post explaining why it is important to not test single methods in isolation:

http://edorian.posterous.com/the-unit-in-unit-testing

Quoting from my post:

Unit testing, in PHP, is about testing the observable behaviors of a class

Most importantly:

Observable from the outside! Nobody cares about the internal state of a class if it never changes the outcome of a method call.


Take this sample:

public function setValue($value) {
    $this->value = $value;
}

public function execute() {
    if (!$this->value) {
        throw new Exception("No Value, no good");
    }
    return $value * 10; // business logic
}

It sounds trivial but the distinction is important and more easy to overlook when looking at bigger classes.

What do we test there?

  • IF we don’t setValue AND then call execute an exception is thrown!
  • IF we DO setValue AND then call execute we get an computed result

So we are testing two behaviors of your class and not the methods in isolation!

Community
  • 1
  • 1
edorian
  • 38,542
  • 15
  • 125
  • 143
  • If you make a single test for the behavior "if we don't setValue and call execute, an exception is thrown", then you have no idea *where* in the code the test failed. For this example, such a situation wouldn't lead to much of a hassle, since you know the problem is in one of two very small methods. But, imagine your one-test-per-behavior was testing a very complex behavior. You could end up having to search half your class code just to track down the issue! Class behaviors are not "the smallest testable pieces of your application", and as such, not units IMO. *Method* behaviors, OTOH... – FtDRbwLXw6 Mar 14 '12 at 19:32
0

IMO you can test behavior of the object. So you can test if has return false before and true after adding some stuff into collection.

l3l0
  • 3,363
  • 20
  • 19