Showing posts with label Amar Parmar. Show all posts
Showing posts with label Amar Parmar. Show all posts

Thursday, 3 March 2016

PHPUnit: trap for dumb players regarding mocking

G'day:
This is just a demonstration of me being a git. But who doesn't like that?

We use PHPUnit extensively to test our codebase. And along with that we use its mocking facilities extensively to mock-out our dependencies in these tests. It's more of a hassle to use than MockBox was on CFML, but this is down to PHP being more rigid with how it goes about things, and is not the fault of PHPUnit (or indeed anyone's "fault" at all: it's just "a thing").

Last week I was flummoxed for a lot longer than I ought to have been, because for some reason a method simply would not mock. After a number of hours I asked the other bods on the team to put their eyes on the code and my mate Amar spotted the problem instantly. The issue was down to me making assumptions about one of our code bases that I was not familiar with, and not bothering to check my assumptions. And also some user-unfriendliness in PHPUnit.

Here's a repro of the code I was running. Well: it bears absolutely no relation to the code I was running at all, but it demonstrates the issue.

Firstly I have a simple service class which has a method I want to test:

namespace me\adamcameron\mocking\service;

class MyService {

    private $logger;

    public function __construct($logger){
        $this->logger = $logger;
    }

    public function doesStuff($value){
        $preppedValue = "PREPPED $value";
        $processedValue = $this->doesRiskyStuff($preppedValue);
        $this->logger->logSomething($processedValue);
        return "RESULT OF DOING STUFF ON $processedValue";
    }

    private function doesRiskyStuff($preppedValue){
        return "$preppedValue (SURVIVED RISKY PROCESS)";
    }

}

This method uses a method from one external dependency, plus it also calls one of its own helper methods which does "risky stuff", so we actually want to mock-out both of those methods.

First things first I already know that one cannot mock private methods with PHPUnit, so we need to make that method protected. This is not ideal, but it's better than not being able to test safely, so it's a burden we are prepared to wear.

Having made that change, we need to partially mock the class we need to test:

function getTestMyService($logger){
    $partiallyMockedMyService = $this->getMockBuilder('\me\adamcameron\mocking\service\MyService')
        ->setConstructorArgs([$logger])
        ->setMethods(["doesRiskyStuff"])
        ->getMock();

    $partiallyMockedMyService->expects($this->once())
        ->method("doesRiskyStuff")
        ->with("PREPPED TEST VALUE")
        ->willReturn("MOCKED RESPONSE FROM DOESRISKYSTUFF");

    return $partiallyMockedMyService;
}

This just mocks-out doesRiskyStuff(), leaving everything else as is. So now when we call doesStuff() in our test, it won't call the real doesRiskyStuff(), but our mock instead.

Notice how some expectations are set on the mocked method itself here:

  • the number of times we expect it to be called;
  • what arguments it will be passed;
  • and a mocked value for it to return.

We call this from our setup() method:

function setup(){
    $logger = $this->getTestLogger();
    $this->myService = $this->getTestMyService($logger);
}


Our test method then is very simple:

/**
 * @covers doesStuff
 */
function testDoesStuff(){
   $result = $this->myService->doesStuff("TEST VALUE");

    $this->assertSame("RESULT OF DOING STUFF ON MOCKED RESPONSE FROM DOESRISKYSTUFF", $result);
}

It simply calls the method and checks what it returns. Simple.

But so far I've glossed over the logging part. 'ere 'tis:

namespace me\adamcameron\mocking\service;

use me\adamcameron\mocking\exception\NotImplementedException;

class LoggingService {

    final public function logSomething($text){
        throw new NotImplementedException(__FUNCTION__ . " not implemented yet");
    }

}


There's not much to that, and indeed I have not even implemented it yet. This is cool... we can just mock it out and test that it receives what it's supposed to be passed by our doesStuff() method. That completes the coverage of doesStuff(): our test of doesStuff() does not need to care whether logSomething() actually works.

Here's how we mock it:

function getTestLogger(){
    $mockedLogger = $this->getMockBuilder('\me\adamcameron\mocking\service\LoggingService')
        ->setMethods(["logSomething"])
        ->getMock();

    $mockedLogger->expects($this->once())
        ->method("logSomething")
        ->with("MOCKED RESPONSE FROM DOESRISKYSTUFF");

    return $mockedLogger;
}


All very obvious and the same as before.

When I run my tests, I see this:

C:\src\php\php.local\www\experiment\phpunit\mock>phpunit
PHPUnit 4.8.23 by Sebastian Bergmann and contributors.

E

Time: 733 ms, Memory: 5.00Mb

There was 1 error:

1) me\adamcameron\mocking\test\service\MyServiceTest::testDoesStuffme\adamcameron\mocking\exception\NotImplementedException: logSomething not implemented yet

C:\src\php\php.local\www\experiment\phpunit\mock\src\service\LoggingService.php:
10
C:\src\php\php.local\www\experiment\phpunit\mock\src\service\MyService.php:16
C:\src\php\php.local\www\experiment\phpunit\mock\test\service\MyServiceTest.php:
23
phpunit\phpunit\src\TextUI\Command.php:149
phpunit\phpunit\src\TextUI\Command.php:100

FAILURES!
Tests: 1, Assertions: 0, Errors: 1.

C:\src\php\php.local\www\experiment\phpunit\mock>


Tuesday, 12 January 2016

PHP & CFML: xpath with empty name spaces

G'day:
My mate who sits next to me at work, Amar, was trying to extract some info from an XML document, and we stumbled over the xpath syntax when there was a namespace defined, but no prefix was given. I'm completely unused to using xpath in PHP (I've had to query something once, I think), but had done a fair bit back in my CFML days.

Here's the XML in question (well: it's not the same XML, but it's equivalent):

<Response xmlns="http://example.com/ns/">
   <user>
      <dateOfBirth>1947-01-08</dateOfBirth>
      <firstName>Ziggy</firstName>
      <lastName>Stardust</lastName>
      <gender>?</gender>
   </user>
</Response>

See we've got a namespace declaration but no bloody prefix defined. Grumble.

On CF9 namespaces could be kinda ignored: just not specifying the namespace at all:

raw  = '<Response xmlns="http://example.com/ns/">
   <user>
      <dateOfBirth>1947-01-08</dateOfBirth>
      <email>sailor@example.com</email>
      <firstName>Ziggy</firstName>
      <lastName>Stardust</lastName>
      <gender>?</gender>
   </user>
</Response>';
xml = xmlParse(raw);

usingEmptyNamespace = xmlSearch(xml, "/:Response/:user/:lastName");
writeDump(usingEmptyNamespace);


Via cflive.net this yields:


Cool. However at some point - it might have been CF10, but I don't know - taking this approach stopped working because ColdFusion changed its XML parsing engine and apparently empty namespaces like that aren't legal.

The solution I had discovered (via googling and Stack Overflow) was to use the local-name() xpath function:

usingLocalName = xml.search("/*[local-name()='Response']/*[local-name()='user']/*[local-name()='firstName']");
writeDump(usingLocalName);

And this yields (I'm using ColdFusion 2016's CLI now), hence the format change:

>cf xpath.cfm
array

1) [xml element]
        XmlName:        firstName
        XmlNsPrefix:
        XmlNsURI:       http://example.com/ns/
        XmlText:        Ziggy
        XmlComment:
        XmlAttributes:  [struct]
        XmlChildren:

>

Now I switch to PHP, and have to make this lot work. Firstly the empty path version simply didn't work:

<?php

$raw = '<Response xmlns="http://example.com/ns/">
   <user>
      <dateOfBirth>1947-01-08</dateOfBirth>
      <firstName>Ziggy</firstName>
      <lastName>Stardust</lastName>
      <gender>?</gender>
   </user>
</Response>
';

$xml = new SimpleXMLElement($raw);

$usingEmptyNamespace = $xml->xpath("/:Response/:user/:gender");
var_dump($usingEmptyNamespace);


>php xpath.php
PHP Warning:  SimpleXMLElement::xpath(): Invalid expression in xpath.php on line 15

Warning: SimpleXMLElement::xpath(): Invalid expression in xpath.php on line 15
bool(false)

>

Using the local-name() approach worked fine in PHP:

$usingLocalName = $xml->xpath("/*[local-name()='Response']/*[local-name()='user']/*[local-name()='firstName']");
var_dump($usingLocalName);



>php xpath.php
array(1) {
  [0]=>
  object(SimpleXMLElement)#2 (1) {
    [0]=>
    string(5) "Ziggy"
  }
}

>

However I was certain PHP would do things better than that, and doing some reading, I see they have a way of resolving the lack of defined namespace, using the registerXPathNamespace() method:

$xml->registerXPathNamespace('db', 'http://example.com/ns/');
$usingRegisteredNamespace = $xml->xpath("/db:Response/db:user/db:lastName");
var_dump($usingRegisteredNamespace);

This allows me to specify a prefix to make the xpath string legit. Nice one!

>php xpath.php
array(1) {
  [0]=>
  object(SimpleXMLElement)#2 (1) {
    [0]=>
    string(8) "Stardust"
  }
}

>

One last thing I tried on a whim in CFML which worked was that it seems one can specify a wildcard namespace:

usingWildcardNamespace = xml.search("/*:Response/*:user/*:firstName");
writeDump(usingWildcardNamespace);


>cf xpath.cfm
array

1) [xml element]
        XmlName:        firstName
        XmlNsPrefix:
        XmlNsURI:       http://example.com/ns/
        XmlText:        Ziggy
        XmlComment:
        XmlAttributes:  [struct]
        XmlChildren:

>

This did not work on PHP. I dunno enough about XML engines and parsing and searching to make a comment on the whys and wherefores of what one's expectations ought to be when it comes to this sort of stuff, but it's good to know about registerXPathNamespace(), and also good to know about the wildcard stuff with CFML.

Not very incisive or groundbreaking stuff, but it's just what I had to work out today.

Righto.

--
Adam

Tuesday, 30 June 2015

JavaScript: deduping an array?

G'day:
I'm being a JavaScript dev at the moment, which is... interesting. My JS ain't bad, but I need to spend a lot of time with my nose in Google  / StackOverflow / MDN.

Here's something that I feel should be easier than I've made it:

Tuesday, 6 January 2015

PHP: help me understand calling a callback in PHP

G'day:
This flummoxed me yesterday, and I wonder if anyone can cast any light on the scene for me.

I've got this sort of situation:

class TestCallback {

    private $callback;

    function __construct($callback){
        $this->callback = $callback;
    }

    // [...]

}