Skip to content

Commit

Permalink
Created a simple JUnit formatter
Browse files Browse the repository at this point in the history
Although it does not handle advanced data, it is JUnit xsd compliant!
  • Loading branch information
gquemener committed Feb 1, 2014
1 parent f8510e3 commit b862f4d
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 122 deletions.
19 changes: 19 additions & 0 deletions features/bootstrap/PhpSpecContext.php
Expand Up @@ -44,6 +44,15 @@ public function iRunPhpspec()
$this->applicationTester->run('run --no-interaction', array('decorated' => false));
}

/**
* @When /^I run phpspec using the "([^"]*)" format$/
*/
public function iRunPhpspecUsingTheFormat($format)
{
$this->applicationTester = $this->createApplicationTester();
$this->applicationTester->run(sprintf('run --no-interaction -f%s', $format), array('decorated' => false));
}

/**
* @When /^(?:|I )run phpspec and answer "(?P<answer>[^"]*)" when asked if I want to generate the code$/
*/
Expand Down Expand Up @@ -98,6 +107,16 @@ public function iShouldSee($message)
expect($this->applicationTester->getDisplay())->toMatch('/'.preg_quote($message, '/').'/sm');
}

/**
* @Then /^I should see valid junit output$/
*/
public function iShouldSeeValidJunitOutput()
{
$dom = new \DOMDocument();
$dom->loadXML($this->applicationTester->getDisplay());
expect($dom->schemaValidate(__DIR__ . '/../../src/PhpSpec/Resources/schema/junit.xsd'))->toBe(true);
}

/**
* @Then /^(?:|the )suite should pass$/
*/
Expand Down
41 changes: 41 additions & 0 deletions features/formatter/use_the_junit_formatter.feature
@@ -0,0 +1,41 @@
Feature: Use the JUnit formatter
In order to provide my CI tool with parsable phpspec results
As a developper
I need to be able to use a JUnit formatter

Scenario: Successfully export phpspec results in JUnit format
Given the spec file "spec/Formatter/SpecExample/MarkdownSpec.php" contains:
"""
<?php
namespace spec\Formatter\SpecExample;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class MarkdownSpec extends ObjectBehavior
{
function it_converts_plain_text_to_html_paragraphs()
{
$this->toHtml('Hi, there')->shouldReturn('<p>Hi, there</p>');
}
}
"""
And the class file "src/Formatter/SpecExample/Markdown.php" contains:
"""
<?php
namespace Formatter\SpecExample;
class Markdown
{
public function toHtml($text)
{
return sprintf('<p>%s</p>', $text);
}
}
"""
When I run phpspec using the "junit" format
Then I should see valid junit output
89 changes: 89 additions & 0 deletions spec/PhpSpec/Formatter/JUnitFormatterSpec.php
@@ -0,0 +1,89 @@
<?php

namespace spec\PhpSpec\Formatter;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use PhpSpec\Formatter\Presenter\PresenterInterface;
use PhpSpec\IO\IOInterface;
use PhpSpec\Listener\StatisticsCollector;
use PhpSpec\Event\SpecificationEvent;
use PhpSpec\Event\ExampleEvent;
use PhpSpec\Loader\Node\SpecificationNode;
use PhpSpec\Event\SuiteEvent;

class JUnitFormatterSpec extends ObjectBehavior
{
function let(
PresenterInterface $presenter,
IOInterface $io,
StatisticsCollector $stats
) {
$this->beConstructedWith($presenter, $io, $stats);
}

function it_stores_a_testcase_node_after_example_run(
ExampleEvent $event,
SpecificationNode $specification,
\ReflectionClass $class
) {
$event->getResult()->willReturn(ExampleEvent::PASSED);
$event->getTitle()->willReturn('example title');

$this->afterExample($event);

$this->getTestCaseNodes()->shouldReturn([
'<testcase name="example title" />'
]);
}

function it_aggregates_testcase_nodes_and_store_them_after_specification_run(SpecificationEvent $event)
{
$event->getTitle()->willReturn('specification title');

$this->setTestCaseNodes(array(
'<testcase name="example1" />',
'<testcase name="example2" />',
'<testcase name="example3" />',
));
$this->afterSpecification($event);

$this->getTestSuiteNodes()->shouldReturn([
'<testsuite name="specification title" tests="3">' . "\n" .
'<testcase name="example1" />' . "\n" .
'<testcase name="example2" />' . "\n" .
'<testcase name="example3" />' . "\n" .
'</testsuite>'
]);
$this->getTestCaseNodes()->shouldHaveCount(0);
}

function it_aggregates_testsuite_nodes_and_display_them_after_suite_run(SuiteEvent $event, $io)
{
$this->setTestSuiteNodes(array(
'<testsuite name="specification1" tests="3">' . "\n" .
'<testcase name="example1" />' . "\n" .
'<testcase name="example2" />' . "\n" .
'<testcase name="example3" />' . "\n" .
'</testsuite>',
'<testsuite name="specification2" tests="2">' . "\n" .
'<testcase name="example1" />' . "\n" .
'<testcase name="example2" />' . "\n" .
'</testsuite>'
));
$this->afterSuite($event);
$io->write(
'<testsuites>' . "\n" .
'<testsuite name="specification1" tests="3">' . "\n" .
'<testcase name="example1" />' . "\n" .
'<testcase name="example2" />' . "\n" .
'<testcase name="example3" />' . "\n" .
'</testsuite>' . "\n" .
'<testsuite name="specification2" tests="2">' . "\n" .
'<testcase name="example1" />' . "\n" .
'<testcase name="example2" />' . "\n" .
'</testsuite>' . "\n" .
'</testsuites>'
)->shouldBeCalled();
}
}
173 changes: 51 additions & 122 deletions src/PhpSpec/Formatter/JUnitFormatter.php
Expand Up @@ -18,164 +18,93 @@
use PhpSpec\Event\SpecificationEvent;

/**
* @author Nick Peirson <nickpeirson@gmail.com>
* The JUnit Formatter
*
* @author Gildas Quemener <gildas.quemener@gmail.com>
*/
class JUnitFormatter extends BasicFormatter
{
/** @var array */
protected $testCaseNodes = array();

/** @var array */
protected $testSuiteNodes = array();

/**
* @var
*/
private $xml;
/**
* @var
*/
private $testSuite;
/**
* @var
*/
private $currentGroup;
/**
* @var
*/
private $currentFile;
/**
* @var int
*/
private $suiteTime = 0;
/**
* @var int
*/
private $assertionCount = 0;
/**
* @var int
*/
private $passCount = 0;
/**
* @var int
*/
private $pendingCount = 0;
/**
* @var int
*/
private $failCount = 0;
/**
* @var int
* Set testcase nodes
*
* @param array $testCaseNodes
*/
private $brokenCount = 0;
public function setTestCaseNodes(array $testCaseNodes)
{
$this->testCaseNodes = $testCaseNodes;
}

/**
* @param SuiteEvent $event
* Get testcase nodes
*
* @return array
*/
public function beforeSuite(SuiteEvent $event)
public function getTestCaseNodes()
{
$this->xml = new \SimpleXMLElement("<testsuites></testsuites>");
return $this->testCaseNodes;
}

/**
* @param SpecificationEvent $event
* Set testsuite nodes
*
* @param array $testSuiteNodes
*/
public function beforeSpecification(SpecificationEvent $event)
public function setTestSuiteNodes(array $testSuiteNodes)
{
$this->currentGroup = $event->getTitle();
$this->currentFile = $event->getSpecification()->getClassReflection()->getFileName();

$this->testSuite = $this->xml->addChild('testsuite');
$this->testSuite->addAttribute('name', $this->currentGroup);
$this->testSuite->addAttribute('file', $this->currentFile);

$this->suiteTime = 0;
$this->assertionCount = 0;
$this->passCount = 0;
$this->pendingCount = 0;
$this->failCount = 0;
$this->brokenCount = 0;
$this->testSuiteNodes = $testSuiteNodes;
}

/**
* @param \SimpleXMLElement $case
* @param ExampleEvent $event
* @param string $exampleTitle
* @param string $failureType
* @param string $failureString
* @param bool $backtrace
* Get testsuite nodes
*
* @return array
*/
private function addFailedTestcase(\SimpleXMLElement $case, ExampleEvent $event, $exampleTitle, $failureType, $failureString, $backtrace = true)
public function getTestSuiteNodes()
{
$failureMsg = PHP_EOL . $exampleTitle
. ' ('.$failureString.')' . PHP_EOL;
$failureMsg .= $event->getMessage() . PHP_EOL;
if ($backtrace) {
$failureMsg .= $event->getException()->getTraceAsString() . PHP_EOL;
}

$error = $case->addChild($failureType, $failureMsg);
$error->addAttribute(
'type',
get_class($event->getException())
);
return $this->testSuiteNodes;
}

/**
* @param ExampleEvent $event
* {@inheritdoc}
*/
public function afterExample(ExampleEvent $event)
{
$title = preg_replace('/^it /', '', $event->getTitle());
$line = $event->getExample()->getFunctionReflection()->getStartLine();
$time = $event->getTime();
$assertions = 0;

$case = $this->testSuite->addChild('testcase');
$case->addAttribute('name', $title);
$case->addAttribute('class', $this->currentGroup);
$case->addAttribute('file', $this->currentFile);
$case->addAttribute('line', $line);
$case->addAttribute('assertions', $assertions);
$case->addAttribute('time', $time);

switch ($event->getResult()) {
case ExampleEvent::PASSED:
$this->passCount++;
break;
case ExampleEvent::PENDING:
$this->addFailedTestcase($case, $event, $title, 'failure', 'PENDING', false);
$this->pendingCount++;
break;
case ExampleEvent::FAILED:
$this->addFailedTestcase($case, $event, $title, 'failure', 'FAILED');
$this->failCount++;
break;
case ExampleEvent::BROKEN:
$this->addFailedTestcase($case, $event, $title, 'error', 'ERROR');
$this->brokenCount++;
break;
}

$this->assertionCount += $assertions;
$this->suiteTime += $time;
$this->testCaseNodes[] = sprintf('<testcase name="%s" />', $event->getTitle());
}

/**
* @param SpecificationEvent $event
* {@inheritdoc}
*/
public function afterSpecification(SpecificationEvent $event)
{
$this->testSuite->addAttribute('tests', $event->getSpecification()->count());
$this->testSuite->addAttribute('assertions', $this->assertionCount);
$this->testSuite->addAttribute('failures', $this->failCount);
$this->testSuite->addAttribute('errors', $this->brokenCount);
$this->testSuite->addAttribute('time', $this->suiteTime);
$this->testSuiteNodes[] = sprintf(
'<testsuite name="%s" tests="%s">' . "\n" .
'%s' . "\n" .
'</testsuite>',
$event->getTitle(),
count($this->testCaseNodes),
implode("\n", $this->testCaseNodes)
);

$this->testCaseNodes = array();
}

/**
* @param SuiteEvent $event
* {@inheritdoc}
*/
public function afterSuite(SuiteEvent $event)
{
$dom = new \DOMDocument('1.0');
$dom->preserveWhitespace = false;
$dom->formatOutput = true;
$dom->loadXml($this->xml->asXml());
$this->getIO()->write($dom->saveXML());
$this->getIo()->write(sprintf(
'<testsuites>' . "\n" .
'%s' . "\n" .
'</testsuites>',
implode("\n", $this->testSuiteNodes)
));
}
}

0 comments on commit b862f4d

Please sign in to comment.