Aspect-based universal Spy

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 680 - 684)

Solution

We need an aspect to intercept every method invocation in a test and record those invocations so that we can verify them later. This is much like method trac- ing, which is a core application of Aspect-Oriented Programming. Many thanks to Ramnivas Laddad, author of AspectJ in Action (Manning, 2003) for his solution, which uses the Wormhole pattern in chapter 8 of that book.

First, let us look at the class under test. To illustrate the power of this tech- nique, we want to verify that a public method invokes a private method, some- thing we would ordinarily need to do by observing some side effect of the private method. In the case of this trivial class, there is no side effect to observe!

package com.mycompany;

public class SomeClass {

public void publicMethod() { privateMethod();

}

private void privateMethod() { } }

We choose to test this using the “Log String” technique that Kent Beck describes in Test-Driven Development: By Example. First we collect the names of the invoked meth- ods, and then we verify the contents of that collection for the methods we expect.

Unlike Kent’s implementation, however, we do not need to implement a mock version of SomeClass that collects the name of each invoked method. Here is the test we want to write.

package com.mycompany.tests;

import com.mycompany.SomeClass;

import java.util.*;

import junit.cookbook.patterns.test.aspectj.UniversalSpyFixture;

import junit.framework.TestCase;

public class SomeClassTest extends UniversalSpyFixture { public void testPublicInvokesPrivate()

throws Exception { setExpectedMethodNames(

Arrays.asList(

new Object[]{"publicMethod", "privateMethod"}));

Listing A.8 Testing SomeClass with the universal Spy

650 APPENDIX A Complete solutions

new SomeClass().publicMethod();

} }

Here we simply specify the names of the methods we expect to be invoked, then invoke the one method that causes the other to be invoked. Think of a long line of dominoes: we expect them all to fall after we topple only the first one. We achieve this method tracing without having to write a mock version of SomeClass. Instead, we use a universal Spy consisting of three parts.

■ The Spy itself, which contains the list of methods invoked during a test

■ The UniversalSpyFixture, a test fixture that contains a universal Spy cap- turing all the relevant method invocations for each test

■ An aspect that intercepts method invocations and records the name of the invoked method (If you are not familiar with AspectJ, we recommend Ram- nivas’s excellent book on the subject.)

In decreasing order of complexity (and therefore interest), let us first look at the aspect in question.

package junit.cookbook.patterns.test.aspectj;

import java.util.*;

import junit.framework.*;

public aspect RecordTestCaseInvokedMethodNames {

public pointcut testExecution(UniversalSpyFixture testCase) : execution(* UniversalSpyFixture+.test*(..))

&& this(testCase);

public pointcut anyMethodInvocation(UniversalSpyFixture testCase) : execution(* *.*(..))

&& cflowbelow(testExecution(testCase))

&& !within(junit.cookbook.patterns.test.aspectj.*);

before(UniversalSpyFixture testCase) : anyMethodInvocation(testCase) {

testCase.getSpy().signalInvokedMethod(

thisJoinPointStaticPart.getSignature().getName());

}

after(UniversalSpyFixture testCase) returning : testExecution(testCase) {

Assert.assertEquals(

Listing A.9 The universal Spy aspect

TE AM FL Y

Team-Fly®

651 Aspect-based universal Spy

testCase.getExpectedMethodNames(),

testCase.getSpy().getInvokedMethodNames());

} }

The pointcut1 anyMethodInvocation() intercepts any method invoked within a test case, excluding methods of the universal Spy machinery itself, which do not interest us anyway. The two pieces of advice are simple enough: before invoking any method within the execution of a test, have the universal Spy record the name of the method about to be invoked. After the test finishes executing, verify that the expected method names (which the test itself sets at some point) matches the collection of methods actually invoked.

The UniversalSpyFixture simply provides a fixture with a universal Spy and a placeholder for the list of method names you expect each test to invoke.

package junit.cookbook.patterns.test.aspectj;

import java.util.*;

import junit.framework.TestCase;

public abstract class UniversalSpyFixture extends TestCase { private List expectedMethodNames;

private UniversalSpy spy = new UniversalSpy();

public List getExpectedMethodNames() { return expectedMethodNames;

}

public void setExpectedMethodNames(List names) { expectedMethodNames = names;

}

public UniversalSpy getSpy() { return spy;

} }

And finally the UniversalSpy itself is a collector for invoked method names.

1 A pointcut is a construct that selects execution points in a program as well as their surrounding context.

For a more detailed definition, and more information about cross-cutting elements in Aspect-Oriented Programming, see Ramnivas Laddad, AspectJ in Action (Manning, 2003), section 2.1.2.

Listing A.10 UniversalSpyFixture

652 APPENDIX A Complete solutions

package junit.cookbook.patterns.test.aspectj;

import java.util.*;

public class UniversalSpy {

private List invokedMethodNames = new ArrayList();

protected void signalInvokedMethod(String methodName) { invokedMethodNames.add(methodName);

}

public List getInvokedMethodNames() {

return Collections.unmodifiableList(invokedMethodNames);

} }

You can reuse this code as is in your tests, as long as you do not mind recording all method invocations inside each test. If you need to narrow your focus to, say, an individual class, then you need to change the aspect RecordTestCaseInvoked- MethodNames, making its pointcuts match only those methods or objects in which you have an interest.

NOTE Building the solution—Compiling aspects and weaving them into source code is a little different than compiling plain vanilla Java code. In particu- lar, you need to compile all the source files and all the desired aspects together at one time.2 If you attempt to compile them separately, then you will not get the desired result.3 You need to compile the code with this command (the important part is the sourceroots value, and not the directory to which the compiled code is to be written):

ajc -sourceroots "com/mycompany;

junit/cookbook/patterns/test/aspectj" -d . Listing A.11 UniveralSpy

2 See Ramnivas Laddad, AspectJ in Action (Manning, 2003), section 3.4, “Tips and tricks.”

3 Worse, as we found out, the test passes even when the required aspect is not weaved into the code. The simplest solution we found was to move the assertion from the aspect into the test to ensure that the assertion is executed and fails when the aspect is not weaved into the code.

653 Test a BMP entity bean

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 680 - 684)

Tải bản đầy đủ (PDF)

(753 trang)