Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to display underlying tested method in PHPUnit tests?

Tags:

php

phpunit

I have test suites with lots of tests in them. here is a medium sized one:

ok  4 - CommodityBasketTest::testStartsOutEmpty
ok  5 - CommodityBasketTest::testCanAddACommodity
ok  6 - CommodityBasketTest::testWillAddOneCommodityByDefault
ok  7 - CommodityBasketTest::testCannotAddACommodityWithAnNonNumericQuantity
ok  8 - CommodityBasketTest::testAddingTheSameCommodityWillIncreaseItsQuantity
ok  9 - CommodityBasketTest::testMultipleCommodityCanBeAdded
ok 10 - CommodityBasketTest::testTakingFromAnEmptyBasketWontWork
ok 11 - CommodityBasketTest::testTakesFirstCommodityFromTheBasket
ok 12 - CommodityBasketTest::testCanRetrieveASpecificCommodity
ok 13 - CommodityBasketTest::testWillThrowExceptionOnMissingCommodity
ok 14 - CommodityBasketTest::testReturnsZeroWorthForEmptyBaskets
ok 15 - CommodityBasketTest::testReturnsProperWorthOfACommodity
ok 16 - CommodityBasketTest::testWillAccuratelyReturnStatistics

How can I rig PHPUnit so that I can somehow display the underlying method being tested like I have it in the paste?? I'm flexible on output; I'd just like to know that CommodityBasketTest::testReturnsZeroWorthForEmptyBaskets tests CommodityBasket::getValuation(), for instance.

This is what I'd like:

-- CommodityBasket::__construct() --
ok  4 - CommodityBasketTest::testStartsOutEmpty

-- CommodityBasket::add() --
ok  5 - CommodityBasketTest::testCanAddACommodity
ok  6 - CommodityBasketTest::testWillAddOneCommodityByDefault
ok  7 - CommodityBasketTest::testCannotAddACommodityWithAnNonNumericQuantity
ok  8 - CommodityBasketTest::testAddingTheSameCommodityWillIncreaseItsQuantity
ok  9 - CommodityBasketTest::testMultipleCommodityCanBeAdded

-- CommodityBasket::take() --
ok 10 - CommodityBasketTest::testTakingFromAnEmptyBasketWontWork
ok 11 - CommodityBasketTest::testTakesFirstCommodityFromTheBasket
ok 12 - CommodityBasketTest::testCanRetrieveASpecificCommodity
ok 13 - CommodityBasketTest::testWillThrowExceptionOnMissingCommodity

-- CommodityBasket::getValuation() --
ok 14 - CommodityBasketTest::testReturnsZeroWorthForEmptyBaskets
ok 15 - CommodityBasketTest::testReturnsProperWorthOfACommodity

-- CommodityBasket::dumpStats() --
ok 16 - CommodityBasketTest::testWillAccuratelyReturnStatistics

Thank you for your suggestions.

like image 509
Theodore R. Smith Avatar asked Jul 15 '11 16:07

Theodore R. Smith


1 Answers

My approach would be a combination of the @covers Tag and a custom result printer.

You should use the @covers tag anyways to generate more meaningful code coverage, especially in bigger test suites it is important to make sure that only the tests that are supposed to test a method really generate coverage for it.

I know your question isn't related to coverage but we'll get to that in a minute. And maybe just using that annotation is enough for you as every method that doesn't have a test dedicated to it will show 0% coverage no matter if you run all your integration tests and so forth.


A custom result listener to collect the information you want

The implementation can surly be tuned, i just wanted to produce something that works reasonably well to show of the concept and hopefully give you something you can adapt.

The code is alpha as I've written it only for that question but it works with the current phpunit and I think i pasted everything you need.

The result:

 --- myClass ---

-- myClass::a --
  ok   - myClassTest::testAone
  fail - myClassTest::testAtwoFails

-- myClass::b --
  ok   - myClassTest::testB

-- myClass::untested --
  !! Method untested !!

I hope this matches your desired output. The formatting can be changed rather easily in the code below.

It prints this piece of information for every class you have a test for (an empty one is enough!)

If one tests @covers multiple methods it will show up for EVERY ONE of those methods


The class under Test

<?php

class myClass {



    public function a() {
        return 1;
    }

    public function b() {
        return 2;
    }

    public function untested() {
        return 3;
    }
}

The Testcase

<?php

require_once("myClass.php");

class myClassTest extends PHPUnit_Framework_TestCase {

    /**
     * @covers a
     */
    public function testAone() {
        $sut = new myClass();
        $this->assertSame(1, $sut->a());
    }

    /**
     * @covers a
     */
    public function testAtwoFails() {
        $sut = new myClass();
        $this->assertSame("error", $sut->a());

    }

    /**
     * @covers b
     */
    public function testB() {
        $sut = new myClass();
        $this->assertSame(2, $sut->b());
    }

}

The phpunit.xml you need to register the test listener!

<phpunit>
    <listeners>
        <listener class="ResultPrinterListener" file="./ResultPrinterListener.php"></listener>
    </listeners>
</phpunit>

The Result Printer

<?php

class ResultPrinterListener implements PHPUnit_Framework_TestListener {

    protected $suites = array();

    public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) {}
    public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) {}
    public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) {}
    public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) {}
    public function startTest(PHPUnit_Framework_Test $test) {}
    public function endTest(PHPUnit_Framework_Test $test, $time) {}
    public function startTestSuite(PHPUnit_Framework_TestSuite $suite) {}

    public function endTestSuite(PHPUnit_Framework_TestSuite $suite) {
        $this->suites[] = $suite;
    }

    public function __destruct() {
        $tests = array();
        foreach($this->suites as $suite) {
            foreach($suite->tests() as $test) {
                if(!$test instanceOf PHPUnit_Framework_TestCase) {
                    continue;
                }

                $testClass = get_class($test);
                $classUnderTest = substr($testClass, 0, -4);  // just cutting the "Test" for now
                /**
                 * Create an array structue
                 *   array[ClassUnderTests][methodUnderTest][arrayOfTestMethodsThatTestThatMethod]
                 * Every method for a class you have at least one test for will show up here for now
                 */
                if(!isset($tests[$classUnderTest])) {
                    if(!class_exists($classUnderTest)) {
                        echo "\nCan't find matching class '$classUnderTest' for test class $testClass!\n";
                    }
                    $class = new ReflectionClass($classUnderTest);
                    foreach($class->getMethods() as $method) {
                        $tests[$classUnderTest][$method->getName()] = array();
                    }
                }
                $annotations = $test->getAnnotations();
                if(!isset($annotations["method"]["covers"])) {
                    continue;
                }

                foreach($annotations["method"]["covers"] as $functionUnderTest) {

                    $statusLine = "";
                    $status = $test->getStatus();
                    if($status == PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE) {
                        $statusLine .= "fail - ";
                    } else if($status == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED) {
                        $statusLine .= "skip - ";
                    } else if($status == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) {
                        $statusLine .= "inc  - ";
                    } else {
                        $statusLine .= "ok   - ";
                    }
                    $statusLine .= $testClass."::".$test->getName();
                    $tests[$classUnderTest][$functionUnderTest][] = $statusLine;
               }
            }
        }
        foreach($tests as $classUnderTest => $methods) {
            echo "\n\n --- $classUnderTest --- \n\n";
            foreach($methods as $method => $testCaseStrings) {
                echo "-- $classUnderTest::$method -- \n";
                if($testCaseStrings == array()) {
                    echo "  !! Method untested !!\n";
                    continue;
                }
                foreach($testCaseStrings as $testCaseString) {
                    echo "  $testCaseString\n";
                }
                echo "\n";
            }

        }
    }

}
like image 192
edorian Avatar answered Nov 15 '22 14:11

edorian