CiviCRM Community Forums (archive)

*

News:

Have a question about CiviCRM?
Get it answered quickly at the new
CiviCRM Stack Exchange Q+A site

This forum was archived on 25 November 2017. Learn more.
How to get involved.
What to do if you think you've found a bug.



  • CiviCRM Community Forums (archive) »
  • Old sections (read-only, deprecated) »
  • Developer Discussion »
  • Unit Testing (Moderator: Michał Mach) »
  • Unit testing the contents of an email
Pages: [1] 2

Author Topic: Unit testing the contents of an email  (Read 6557 times)

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Unit testing the contents of an email
October 23, 2012, 03:28:41 pm
I'm just wondering if anyone has already tackled this? What I want to do is test whether an outbound email generated on an activity save contains what it's supposed to. It needs to be a webtest, because the email is only generated from the form.

If nobody's already done this, then some options I can think of:

- Have it set CIVICRM_MAIL_LOG, then parse the output of the log file.

- In this particular case I'm mostly interested in an attachment, so figure out how to have the unit test implement one of the civi hooks, and then check the attachments. I can see a CRM_Utils_Hook_UnitTests, but I'm not sure how to use it since it seems like a stub? Is this part of http://forum.civicrm.org/index.php/topic,26358.msg111755.html#msg111755 ? Or is now the standard way to call a hook from a core test to write an extension to go along with the core test?

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Unit testing the contents of an email
October 23, 2012, 04:25:11 pm
a) I think it makes complete sense to use a mock mail service during unit-testing. Something richer than CIVICR_MAIL_LOG would be nice, but I think CIVICRM_MAIL_LOG would work anyway.

b) I like the idea of updating CRM_Utils_Hook_UnitTests to allow mocking of hooks. It only requires major changes to that one file:

Code: [Select]
<?php
class CRM_Utils_Hook_UnitTests {
  static 
$mockObject;
  static 
$adhocHooks;

  
// Call this in CiviUnitTestCase::setUp()
  
static function reset() {
    
self::$mockObject = NULL;
    
self::$adhocHooks = array();
  }

  
/**
   * Use a unit-testing mock object to handle hook invocations
   * e.g. hook_civicrm_foo === $mockObject->foo()
   */
  
static function setMock($mockObject) {
    
self::$mockObject = $mockObject;
  }

  
/**
   * Register a piece of code to run when invoking a hook
   */
  
static function setHook($hook, $callable) {
    
self::$adhocHooks[$hook] = $callable;
  }
  
  function 
invoke($numParams, &$arg1, ..., $fnSuffix) {
    if (
self::$mockObject) {
      
call_user_func(array(self::$mockObject, $fnSuffix), $arg1, ...);
    }
    if (
self::$adhocHooks[$fnSuffix]) {
      
call_user_func(self::$adhocHooks[$fnSuffix], $arg1, ...);
    }
  }
}

This would allow you to write unit-test code like:

Code: [Select]
<?php
  
function testHookPreAndPost() {
    
$mock = $this->getMock('stdClass', array('pre', 'post'));
    
$mock->expects($this->once())
      ->
method('pre');
    
$mock->expects($this->once())
      ->
method('post');
    
CRM_Utils_Hook_UnitTests::setMock($mock);
    
CRM_Contact_BAO_Contact::create(...);
  }

or like

Code: [Select]
<?php
  
function testHookPre_ContactCreation() {
    
$test = $this;
    
CRM_Utils_Hook_UnitTests::setHook('pre', function($op, $objectName, $id, &$params) use ($test){
      
$test->assertEquals('create', $op);
      
$test->assertEquals('Individual', $objectName);
      
$test->assertEquals(NULL, $id);
    });
    
CRM_Utils_Hook_UnitTests::setHook('post', function($op, $objectName, $id, &$params) use ($test){
      
$test->assertEquals('create', $op);
      
$test->assertEquals('Individual', $objectName);
      
$test->assertTrue(is_numeric($id) && $id>0);
    });
    
CRM_Contact_BAO_Contact::create(...);
  }

c) The CRM_Utils_Hook_UnitTests approach seems better to me (more fluent/natural) than creating extra extensions, it has a couple drawbacks: it doesn't work with web-tests, and it doesn't work with extension-lifecycle hooks (hook_civicrm_install, hook_civicrm_enable, etc). For those cases, making a dummy extension is necessary. Like this:

https://fisheye2.atlassian.com/browse/CiviCRM/branches/trunk.extsplit/tests/phpunit/CRM/Extension/Manager/ModuleTest.php?hb=true
https://fisheye2.atlassian.com/browse/CiviCRM/branches/trunk.extsplit/tests/extensions/test.extension.manager.moduletest

Note that this is only possible following CRM-11045 -- which is a work-in-progress (branches/trunk.extsplit) which targets 4.3. It will probably be merged into trunk this week.

Eileen

  • Forum Godess / God
  • I’m (like) Lobo ;)
  • *****
  • Posts: 4195
  • Karma: 218
    • Fuzion
Re: Unit testing the contents of an email
October 24, 2012, 04:44:28 am
Far less clever approach than Tim's - but I did add unit tests on the Contribution api & Base IPN based

http://svn.civicrm.org/civicrm/trunk/tests/phpunit/api/v3/ContributionTest.php

- look at the sendMail one (which sends out a contribution receipt)
Make today the day you step up to support CiviCRM and all the amazing organisations that are using it to improve our world - http://civicrm.org/contribute

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Re: Unit testing the contents of an email
October 24, 2012, 10:34:22 am
Thanks for the suggestions (I did a grep on CIVICRM_MAIL_LOG to see if anyone else had done it but guess I didn't include the parent class directory).

For adding to the Hook_UnitTests, I wonder if doing it by function naming convention similar to the way hooks normally get called might be easier for people used to the current model? So for example, assuming it's possible for that invoke function to know which test is currently running, then all the unit test would have to do is implement a function called, say, testHook_civicrmPostProcess, and then the higher level invoke function would check for the existence of CurrentlyRunningTest::testHook_XXX() and call it.  Not sure if it would need to be a static func just trying to write out what I'm thinking.

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Unit testing the contents of an email
October 24, 2012, 01:47:34 pm
So are you suggesting something more like this? (Note: I've slightly modified the naming convention to avoid accidental conflicts with test functions.)

Code: [Select]
<?php
class CRM_HookNamingConventionTest {
  function 
setUp() {
    
CRM_Utils_Hook_UnitTest::setUp($this);
  }
  function 
testFooBar() {
    
$this->hookState = 0;
    
CRM_Contact_BAO_Contact::create(...); // create new contact
    
CRM_Contact_BAO_Contact::create(...); // update contact
    
$this->assertEquals(4, $this->hookState);
  }
  function 
hookFooBar_civicrm_pre($op, $objectName, $id, &$params) {
    switch (++
$this->hookState) {
      case 
1:
        
$this->assertEquals('create', $op);
        
$this->assertEquals('Individual', $objectName);
        break;
      case 
3:
        
$this->assertEquals('edit', $op);
        
$this->assertEquals('Individual', $objectName);
        break;
      default:
        
$this->fail("Bad call");
    }
  }
  function 
hookFooBar_civicrm_post($op, $objectName, $id, &$params) {
    switch (++
$this->hookState) {
      case 
2:
        
$this->assertEquals('create', $op);
        
$this->assertEquals('Individual', $objectName);
        break;
      case 
4:
        
$this->assertEquals('edit', $op);
        
$this->assertEquals('Individual', $objectName);
        break;
      default:
        
$this->fail("Bad call");
    }
  }
}

Note that it takes some care to get the assertions right -- it's easy to accidentally write a test which says "If hook X is called, then check it's parameters with logic Y" -- but the hook might never run or might run too many times. It's harder to say "Hook X is called once with parameter A, then hook Y is called once with parameter B, and then hook Y is called again with parameter C". I think the 'mock object' approach deals with this better, although I've had some trouble finding a tutorial on PHPUnit that covers all the right material. I've tried to piece together a better example. The key thing to note is that it affirmatively ensures that the hooks are run the correct number of times.

Code: [Select]
<?php
class CRM_MockHookTest {
  function 
setUp() {
    
CRM_Utils_Hook_UnitTest::setUp($this);
  }
  function 
testFooBar() {
    
$hook = $this->getMock('stdClass', array('pre', 'post'));
    
CRM_Utils_Hook_UnitTest::setMock($hook);

    
$hook->expects($this->at(1))
      ->
method('pre')
      ->
with('create', 'Individual', $this->anything(), $this->isType('object'));
    
$hook->expects($this->at(2))
      ->
method('post')
      ->
with('create', 'Individual', $this->isType('int'), $this->isType('object'));
    
CRM_Contact_BAO_Contact::create(...); // create new contact

    
$hook->expects($this->at(3))
      ->
method('pre')
      ->
with('edit', 'Individual', $this->isType('int'), $this->isType('object'));
    
$hook->expects($this->at(4))
      ->
method('post')
      ->
with('edit', 'Individual', $this->isType('int'), $this->isType('object'));
    
CRM_Contact_BAO_Contact::create(...); // update contact
  
}
}

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Re: Unit testing the contents of an email
October 24, 2012, 05:33:59 pm
Close to what I was thinking, but good point. Although since the caller knows what they've set up in the test they could differentiate based on what they're expecting at that point:

Code: [Select]

function hookFooBar_civicrm_pre($op, $objectName, $id, &$params) {
  if ($op == 'create' && $id == 2 && $params['x'] == 'y' etc ) {
    // assert stuff
  } elseif ($op == 'edit' && $id == 7 && $params['something'] == 'something' etc) {
    // assert other stuff
  }
}

But it might be good to be explicit and have separate callbacks handle each invocation, in which case I like your second original example, the one like this:
Code: [Select]
CRM_Utils_Hook_UnitTests::setHook('pre', function($op, $objectName, $id, &$params) use ($test){
      $test->assertEquals('create', $op);
      $test->assertEquals('Individual', $objectName);
      $test->assertEquals(NULL, $id);
    });

Then another question is what handles clearing the registered hook from $adHocHooks, because otherwise invoke() will keep calling it until you call another set(). Since probably 99.9% of the time clearing it is what's desired could just unset(self::$adhocHooks[$fnSuffix]) at the end of invoke().

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Unit testing the contents of an email
October 25, 2012, 10:51:06 am
Yeah, doing separate callbacks feels more fluid to me, but if one does want to do a separate named function, then they could still do it with setHook like:

Code: [Select]
<?php
class CRM_SeparateHookFunctionTest {
  function 
testFooBar() {
    
CRM_Utils_Hook_Unit::setHook('pre', array($this, 'hookFooBar_civicrm_pre'));
  }
  function 
hookFooBar_civicrm_pre($op, $objectName, $id, &$params) {
    ..
  }
}

Agree that 99% of the time you want to clear the hook at the end of the test. I think we would do this automatically by updating CiviUnitTestCase::setUp() and CiviUnitTestCase::tearDown() to call CRM_Utils_Hook_UnitTests::reset().

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Re: Unit testing the contents of an email
October 25, 2012, 11:33:06 am
For clearing I mean there are some tests that create two individuals in the same test. If you don't setHook again before the second one or clear $adHocHooks['pre'] somehow, then the second individual creation would also trigger the callback that was meant for the first one. And in the case of 'pre', pretty much anything could trigger it.

Maybe overthinking it ... Seems like it's all a good enough idea to try and see what happens.

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Re: Unit testing the contents of an email
October 30, 2012, 01:43:50 pm
Just a followup on using CIVICRM_MAIL_LOG - it doesn't seem to get recognized when run under WebTests, but I don't know why so will look closer at that. I added tests/phpunit/CiviTest/CiviMailService.php as its own class since the intention was so that it could be used from both WebTest and non-WebTest. IcalTest.php in tests/phpunit/WebTest/Activity is where I was going with it (the test currently fails).

Donald Lobo

  • Administrator
  • I’m (like) Lobo ;)
  • *****
  • Posts: 15963
  • Karma: 470
    • CiviCRM site
  • CiviCRM version: 4.2+
  • CMS version: Drupal 7, Joomla 2.5+
  • MySQL version: 5.5.x
  • PHP version: 5.4.x
Re: Unit testing the contents of an email
October 30, 2012, 01:53:21 pm

Note that for webtests, the civicrm.settings.php is the one used at the webserver url level, which is definitely not your local phpunit civicrm.settings.php

lobo
A new CiviCRM Q&A resource needs YOUR help to get started. Visit our StackExchange proposed site, sign up and vote on 5 questions

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Re: Unit testing the contents of an email
October 30, 2012, 02:15:48 pm
In conversation with Lobo, there are actually two separate php instances during a webtest - the testing framework's and the instance running on your actual test site.

The current plan is to have a UI setting to store it in the db to override CIVICRM_MAIL_LOG - ticket is at http://issues.civicrm.org/jira/browse/CRM-11190

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Re: Unit testing the contents of an email
October 31, 2012, 09:41:47 am
There was a suggestion to override CRM_Utils_Mail, but I'm not sure what "override" means in this context, because the problem is that in webtests what I would want is access to the instance of CRM_Utils_Mail that's running on the server, not the test framework's instance which is the one the test has access to. In webtests all you have access to from the server's instance is what appears in the browser window.

So unless I'm misunderstanding the suggestion, I'm still thinking about a config setting, exposed in the UI. And that config setting would of course be available to non-webtests too.

Then, I kind of like the suggestion of writing the emails out to the db instead of a log file, and then provide a function to retrieve them and a url to display them in the browser window, which would also satisfy webtest needs.

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Unit testing the contents of an email
October 31, 2012, 10:35:39 am
Agree that overriding CRM_Utils_Mail in an OOP sense would not help. Adding a config setting (and updating CRM_Core_Config::getMailer() to respect it) makes sense. (This will effectively change the behavior of CRM_Utils_Mail as well as the BAOs in CRM_Mailing.)

FWIW, the web-test process may be able to access the DB without going through a web request. (At least, there are several helpers like CiviSeleniumTestCase::assertDBState.) That may be easier than writing a new web-page.

But if you want to do a web UI, that's cool -- I think there are other use-cases for that, too. For example, on staging sites (where developers and non-developers work with pre-release system), the "spool-to-disk" works OK for developers but not for other testers. There are also times when admins want to see an exact replica of a message that was sent out. (I've esp seen this on smaller projects that don't have a rigorous staging/production process.) (For the second use-case, the configuration setting for "Logging-to-DB" would need to be separate from the configuration setting for "outBound_option" so that admin could choose any combination of logging/delivering.)

Eileen

  • Forum Godess / God
  • I’m (like) Lobo ;)
  • *****
  • Posts: 4195
  • Karma: 218
    • Fuzion
Re: Unit testing the contents of an email
October 31, 2012, 11:55:25 am
I agree with moving that setting. But, as a quick way to get started you could use

CiviUnitTestCase::prepareMailLog

It does assume you have not otherwise defined mail log as 1 but that's not too problematic in the context of tests.

Code: [Select]
  /*
   * Empty mail log in preparation for test
   */
  function prepareMailLog(){
    if(!defined('CIVICRM_MAIL_LOG')){
      define( 'CIVICRM_MAIL_LOG', CIVICRM_TEMPLATE_COMPILEDIR . '/mail.log' );
    }
    $this->assertFalse(is_numeric(CIVICRM_MAIL_LOG) ,'we need to be able to log email to check receipt');
    file_put_contents(CIVICRM_MAIL_LOG,'');
  }

Code: [Select]
  function testSendMail() {
    $this->prepareMailLog();
    $contribution = civicrm_api('contribution','create',$this->_params);
    $this->assertAPISuccess($contribution);
    $apiResult = civicrm_api('contribution', 'sendconfirmation', array(
      'version' => $this->_apiversion,
      'id' => $contribution['id'])
    );
    $this->assertAPISuccess($apiResult);
    $this->checkMailLog(array(
        '$ 100.00',
        'Contribution Information',
        'Please print this confirmation for your records',
      ), array(
        'Event'
      )
    );
  }
Make today the day you step up to support CiviCRM and all the amazing organisations that are using it to improve our world - http://civicrm.org/contribute

demeritcowboy

  • Ask me questions
  • ****
  • Posts: 570
  • Karma: 42
  • CiviCRM version: Always the latest!
  • CMS version: Drupal 6 mostly, still evaluating 7.
  • MySQL version: Mix of 5.0 / 5.1 / 5.5
  • PHP version: 5.3, usually on Windows
Re: Unit testing the contents of an email
November 01, 2012, 03:23:06 pm
Thanks yeah that function was what I used when I first started testing it. I was going to move them into the CiviMailService - I will probably update those contribution tests to use whatever the new thing I end up with as a way to see if it also works for non-webtests.

Pages: [1] 2
  • CiviCRM Community Forums (archive) »
  • Old sections (read-only, deprecated) »
  • Developer Discussion »
  • Unit Testing (Moderator: Michał Mach) »
  • Unit testing the contents of an email

This forum was archived on 2017-11-26.