This is Part 2 of a series on how to test SugarCRM logic hooks. Part 1 showed how to make an untestable logic hook testable in 5 easy steps. In this part we will create a test suite for our refactored logic hook.
Testing the Code
We’ve completed the process of making our logic hook testable, so let’s start testing!
Notice that we have not really changed the original code in any substantial way. This is good in that our changes so far are likely to cause no damage. However, some things will be more difficult to test as the code stands at present. For example, if
$this->bean→region_c is empty or
$this->bean→expected_close_c is empty, no relationship should be created.
Since our method
testableRelateOppToForecast() returns no value, how can we tell whether the relationship was created or not?
Testing for Both Behavior and Assertion
Unit tests are generally reasoned about in terms of assertions. And assertions are great for verifying values. OOP embraces functions and procedures without distinction. Functions return values and procedures have side effects. Typical OOP code does either or both at the same time.
How do you test a side effect? And what exactly is a side effect anyway? And why should I care?
Side effects are things that often happen at a distance. Think of a database update or an external http request. With the magic of “mocks,” we can test whether a database was accessed and how it was accessed. These are behaviors and they can be tested. Side effects can be tested by their behavior.
Our code does not return a value, but it does create a log entry when a relationship is created. In lieu of a return value to assert, we can check whether the behavior of writing a log entry happened or not.
In summary, test for assertions and/or behaviors. They are both useful. And since we need to mock some collaborators anyway, it’s no big deal to test expectations on mocks.
Let’s Write Tests
Now that we have isolated code to test, we’ll follow a simple pattern.
If we weren’t testing, we’d simply create the object and call
relateOppToForecast($bean, $event, $arguments). This method is not testable. But since we moved the logic to
testableRelateOppToForecast(), we'll follow this pattern instead:
- Create an instance of the class. This doesn’t do anything at all except give us an object to call methods on. It has no constructor so there’s no danger or possibility of any action on the new object until a method call happens.
init($bean, $event, $arguments, $forecast, $sql, $logger)with any necessary mocks.
testableRelateOppToForecast()and verify any assertions and/or expectations.
Building a Test Suite
I will use Codeception and Mockery for tests. You can do the same with plain PHPUnit or any other tool of choice. Pick your poison. We’ll eventually look at the entire suite. It might look long, but there’s a fair bit of repetition here and we’ll take it in bites.
First up, set up some mocks and test that a relationship is created if there is a region and a close date.
Now, let’s break this down a bit…
_before() method is responsible to initialize our test environment. It will create 4 mocks that will be passed to a freshly minted instance of our logic hook’s
init() method. Pretty simple so far.
The test itself,
testOppWithNonEmptyRegionAndExpectedCloseShouldBeLinked(), will initialize some attributes on the mocks and create some expectations. In particular, we expect the SQL mock to receive the specified method calls and return a value of
1 from the
execute() method. This is a behavioral test and proves that our logic hook is making database calls as expected.
$this->opp->fore_forecasting_opportunities_1 = m::mock('stdClass'); is unusual and deserves a bit of explanation. The mock
opp, which stands in for the bean, will have an attribute
fore_forecasting_opportunities_1 attached in the real code that will itself expect a call to the method
add() (reference the 4th statement from the bottom in the original code.)To test this behavior, we’ll create an ad-hoc mock and set an expectation that it should be called with an
The next line sets an expectation that the mocked bean should be called with a
Finally, we verify that the mocked logger will be called with an
info() method that will will verify that the log message “relateOppToForecast success!” was indeed logged.
Here’s an excerpt from the source code showing the unusual behavior described in the previous paragraph. Note the last few lines before the logger statement. This is very SugarCRM specific and probably confusing. Nonetheless, we can quite thoroughly test our logic hook’s behavior and gain confidence that it is behaving properly.
The key takeaway is that you don’t have to know the low-level details of what the code is doing to test it. Even though the code is not providing something to assert against, it is behaving in a certain way and you can test for that behavior.
Whew! If you understand this much, the rest of the test suite will all make sense.
The Power of Mocks and Unit Tests
If you are puzzled about mocks and expectations, take some time and get the hang of them. They are really powerful!
I have had many conversations with talented developers who don’t understand or use mocks. My advice is to learn how to use them and what they are good for. We have seen how we can test behavior with expectations on mocks. The alternative, writing to a database and changing the state of the test environment, has its own problems including poorer test performance and the need to reset the test environment and database state after allowing a test to modify it. Read the docs for Mockery and PHPUnit mocks. Go through some tutorials. Mostly get some experience. I find the best way to get experience is to decide to write tests when you write code. It boils down to a discipline that reaps rewards the more you practice.
Simplification When All Your Logic is in One Method
The process described so far works for complex multi-method logic hooks. But you don’t always need to go to these lengths. Here is an acceptable simplification where we skip step 1 and step 4. Call it: “Test (almost) ANY Sugar logic hook in 3 simple steps” if you want. I’ll also add one little addition to the logic to make it possible to create an assertion.
So far we have completely ignored assertions and tested the logger behavior instead.
Note that we didn’t bother with instance variables at all! Also, the logic hook now returns a
true/false value that we can assert on.
Here’s the first test:
Feel free to use this simplification. Simpler is usually better.
For reference, here’s the entire test suite:
Sugar logic hooks are absolutely unit testable with a bit of work. The pattern described here can be used to test code that was not written to be testable.
It’s obviously useful for Sugar logic hooks, but you’ll find many other opportunities to use these ideas on legacy code in general. The key idea is to isolate the code you wish to test from everything else which is the essence of a pure unit test. And this process shows a way to get there with minimal changes to your code and minimal opportunities to cause problems along the way.
Making code testable is a prerequisite for having well-tested code. You still have to write the tests and sometimes you need assertions while sometimes you need to test behavior. A well-armed developer uses both tools as needed.