Recipe 20.4. Applying Mock Components to Support Unit TestingProblemYou want to unit test your own component in isolation by providing a test harness implementation of a component that your component uses. SolutionCreate a mock implementation of the external component that your component depends on. Create an aspect that applies the mock component implementation in place of the real component. When unit testing is complete, use a separate AspectJ build configuration file to switch out the testing aspect so the real implementation can be used again. DiscussionWhen you are creating your software components, unit test those components in isolation to ensure that the component works as it should. However, when your component relies on other components, it can be tricky and error prone to manually unhook all of the dependencies to the external components for the unit testing and to restore those hooks when unit testing is complete. Example 20-9 shows a typical situation where MyComponent is the component to be tested, and it has dependencies on an external implementation of ThirdPartyComponentInterface . The real implementation of ThirdPartyComponentInterface is retrieved by making a call to the factory method ThirdPartyFactory.getThirdPartyComponent(). Example 20-9. Creating and using an external componentpackage com.ourcompany; import com.thirdparty.ThirdPartyComponentFactory; import com.thirdparty.ThirdPartyComponentInterface; public class MyComponent implements MyComponentInterface { private ThirdPartyComponentInterface thirdPartyComponent; public MyComponent( ) { this.thirdPartyComponent = ThirdPartyComponentFactory. getThirdPartyComponent( ); System.out.println("Component found " + thirdPartyComponent); } public void foo( ) { System.out.println("Inside MyComponent.foo( )"); this.thirdPartyComponent.bar( ); } }
To run a unit test against MyComponent in isolation, you will need to override the ThirdPartyComponent implementation so you will not confuse the test results by including the real external component in the test. One strategy is to apply a mock component that overrides the real component implementation manually, as shown in Example 20-10. Example 20-10. Creating a mock class and then manually amending your component to use the new mock objectpackage test.com.ourcompany; import com.thirdparty.ThirdPartyComponentInterface; public class MockThirdPartyComponent implements ThirdPartyComponentInterface { public void bar( ) { System.out.println("Inside MockThirdPartyComponent.bar( )"); } } package com.ourcompany; import com.thirdparty.ThirdPartyComponentFactory; import com.thirdparty.ThirdPartyComponentInterface; import test.com.ourcompany.MockThirdPartyComponent; public class MyComponent implements MyComponentInterface { private ThirdPartyComponentInterface thirdPartyComponent; public MyComponent( ) { // Manually commented out for unit testing purposes // this.thirdPartyComponent = // ThirdPartyComponentFactory.getThirdPartyComponent( ); // Replaced with mock object this.thirdPartyComponent = new MockThirdPartyComponent( ); System.out.println("Component found " + thirdPartyComponent); } public void foo( ) { System.out.println("Inside MyComponent.foo( )"); this.thirdPartyComponent.bar( ); } } The MockThirdPartyComponent is under your control, unlike the real implementation of the ThirdPartyComponentInterface, so you can tailor the component to test your MyComponent correctly. The MyComponent is no longer reliant on the real implementation of the ThirdPartyComponentInterface, so you can safely conclude that any problems that occur when it is tested are your problems. After your unit testing is complete and you are satisfied that MyComponent is working correctly, the mock component can be removed and the original code uncommented. Using this method, switching back to the real implementation of ThirdPartyComponentInterface is another manual and potentially error-prone task. Though the solution in Example 20-10 works fine, it can be a difficult approach to manage if your component uses more than a few interfaces. Using an aspect-oriented alternative, an aspect can be created that intercepts the creation of the ThirdPartyComponent and overrides the returned object with your mock object implementation, as shown in Example 20-11. Example 20-11. Using an aspect to apply a mock object for isolation unit testing purposespackage test.com.ourcompany; import com.thirdparty.*; public aspect MockThirdPartyComponentAspect { public pointcut catchThirdPartyConstructor( ) : call(ThirdPartyComponentInterface ThirdPartyComponentFactory. getThirdPartyComponent( )); Object around( ) : catchThirdPartyConstructor( ) { return new MockThirdPartyComponent( ); } The catchThirdPartyConstructor() pointcut catches calls to the ThirdPartyFactory.getThirdPartyComponent( ) method and uses the associated around( ) advice to return your MockThirdPartyComponent object rather than the real implementation of the ThirdPartyComponentInterface. By using aspects rather than manual code changes, your component is prepared for isolation unit testing automatically after the aspect is woven into your application.
You can switch between using the real or mock implementation of the external component at compile time by creating two different AspectJ build configuration files. A build configuration file that includes the mock object testing aspect will result in a build that will be all set for isolation unit testing of your component, and a build configuration file that excludes the mock object testing aspect will result in a deployment build that uses the real implementation of the external component. AspectJ makes the task of applying mock objects for testing purposes less intrusive to your code, much more scalable and much less of a headache when it comes to switching between testing and deployment builds. See AlsoUsing different build configurations to vary the aspects applied to an application is explained in Recipe 2.5; the call(Signature) pointcut is described in Recipe 4.1; the around( ) form of advice is covered in Recipe Recipe 13.4; how to implement the Abstract Factory and Factory Method design patterns using AspectJ is shown in Recipes Recipe 17.3 and Recipe 17.4, respectively; the Cuckoo's Egg aspect-oriented design pattern is discussed in Recipe 23.1. |