Method stubs allow you to define in Exemplars and on JUnits (via @Stubs,) what a given method should return at runtime in the testing context. You can direct the stub to take effect on every invocation of a given method, or only when the method parameters match a given expression.
You can stub any method in your project code (source path), but not library classes. If you need to stub an imported library class, one option is to create a delegate class to access the library class and stub that. You could also use source stubbing or in some scenarios, default classes – these are covered later.
5.2.1. Simple Method Stubs
Consider a booking system method that cancels a given booking. This calls a method on a service delegate that calls out to a remote service EJB to cancel a seat:
@Exemplar(args={"Booking/i1"}, expect="$arg1.isCancelled") public void cancelBooking(Booking booking) throws SeatNotFoundException { ... try { List seatIDs = booking.getSeatIDs(); for (int seatID : seatIDs) { seatServiceDelegate.cancelSeat(seatID); } booking.setCancelled(); } catch (NamingException e) { throw new TechnicalException(e); } catch (RemoteException e) { throw new TechnicalException(e); } ... }
public class SeatServiceDelegate { InitialContext ctx; ... public void cancelSeat(int seatID) throws NamingException, RemoteException, SeatNotFoundException { // Access remote service to cancel the given seat SeatService service = (SeatService) ctx.lookup("SeatService"); service.cancel(seatID); } ... }
The Exemplar will result in a NamingException or RemoteException if the remote service is not accessible, which will be the case in a unit testing context. If it were accessible, this would not be a unit test as it has a dependency on an external resource to function.
In order to test cancelBooking, we need to stub out the SeatService. Our test needs dictate the behaviour of the cancelSeat method; in doing so it can isolate the method-under-test from external dependencies. We can achieve this with a simple method stub definition in our Exemplar:
@Exemplar(args={"Booking/i1"}, expect="$arg1.isCancelled", stubs="SeatServiceDelegate.cancelSeat") public void cancelBooking(Booking booking) throws SeatNotFoundException {
Adding the stubs=”SeatServiceDelegate.cancelSeat” property to the Exemplar will effectively remove the cancelSeat method call. The Exemplar will then pass as the cancelBooking method will progress to call setCancelled and our expect condition $arg1.isCancelled will return true.
This is the simplest example of method stubbing: we match all invocations of a given method (SeatServiceDelegate.cancelSeat) and remove them. If the stubbed method returned something other than void, we’d have to define what the stub should return. Furthermore the method may be invoked several times, and we might want a different result for each invocation. In addition, we might want to return different values depending on the arguments passed in to the stubbed method.
The rest of this section describes how to use method stubs in these more complex scenarios.
5.2.2. The Stub Match Expression
Stub expressions are of the form:
matchExpression = resultExpression
The match expression determines what method invocations should be stubbed. The match expression takes the form:
className.methodName(arg1Matcher, arg2Matcher , …)
You can use just the simple class name rather than fully-qualified for any project class.
5.2.2.1. The arg Named Instance and Default Argument Matching
To stub a method that has arguments, typically you would specify the class or type of each argument:
For example to stub the following method:
public class SeatServiceDelegate { public int book(List seats, Customer customer, int agencyID); }
You would use the stub expression:
SeatServiceDelegate.book(List,Customer,int)
It is important to note, that each of the method arguments given here is actually using the default argument matcher.
That is: isa(arg,X) where arg is the object or value passes as the argument, and X is the argument match expression given, which was the class name to match (isa is like Java instanceof; see the SIN Operators section for details). In other words, these two match expressions are the same:
SeatServiceDelegate.book(List,Customer,int) SeatServiceDelegate.book(isa(List),isa(Customer),isa(int))
The significance of this is that your stub match expression doesn’t need to match all invocations of the method to stub. Let’s say that you only want to stub out SeatServiceDelegate.book when the value of the agencyID parameter is 2. You would specify:
SeatServiceDelegate.book(List,Customer,=(arg,2))
Or perhaps you only want the stub to kick in when the customer’s name is Fred and the agencyID is over 1:
SeatServiceDelegate.book(List, arg.name.equalsIgnoreCase(“Fred”), >(arg,1))
Using stub argument matchers like this, you can define different stub results for different invocations of the stubbed methods, depending on the value of the parameters passed in.
5.2.2.2. The Wildcard Matcher and Omitting Matcher Expressions
You can use the wildcard matcher “*” to match any argument value or type, so:
SeatServiceDelegate.book(*, *, *)
Would match any invocation of SeatServiceDelegate.book that passed 3 parameters.
Futhermore, you do not have to specify parameters at all. Specifying just:
SeatServiceDelegate.book
Would match any invocation of any method in SeatServiceDelegate named book.
Be careful with using the wildcard in stub expressions though: there could be several methods in a class with the same name and number of parameters, but with differing return types. And even if there isn’t, a method might be overloaded at a later date which could break your stub.
Wildcard matchers are more useful in verify expressions. See the Behaviour Verification section for details of these.
5.2.3. The Stub Result Expression
Stub expressions are of the form:
matchExpression = resultExpression
The result expression defines what is returned by the stubbed method. It can only be specified for non-void methods, or when you want to throw an exception from the stub.
5.2.3.1. Specifying a Result
You can specify a value to return from the stub, or any expression that returns a value that is compatible with the return type of the stubbed method.
For example to stub this method and return the value 1 from it:
public class SeatServiceDelegate { public int book(List seats, Customer customer, int agencyID); }
stubs="SeatServiceDelegate.book(List,Customer,int) = 1"
Or you may want to call a method in your test code that gets you a value to return from the stub:
stubs="SeatServiceDelegate.book(List,Customer,int) = TestUtils.bookStub()"
5.2.3.2. Using the Stub Method Instance and Arguments in the Result Expression
If your stub result expression is calling a method to get the stub result, it’s likely you’ll want need to give it information about the stubbed method. You can use the obj and arg1..n named instances to refer to the object being invoked and the arguments of the stubbed method call respectively. For instance you might want to generate a return value from the stub in test code, based the stubbed method’s argument values and the object on which the method was called:
stubs="SeatServiceDelegate.book(List,Customer,int) = TestUtils.bookStub(obj,arg1,arg2,arg3)"
Then in TestUtils:
public class TestUtils { public static int bookStub(SeatServiceDelegate instance, List seats, Customer customer, int agencyID) { for (Seat seat : seats) { seat.setBooked(true); } // ... work with instance, seats, customer and/or agencyID as required ... return seats.size(); } }
5.2.3.3. Specifying Multiple Results with a Stub List
Sometimes, you may want your stub to return different results, or behave differently, for each invocation of the stubbed method.
You can specify different results for each stub invocation by using the special stub list SIN Type: [sl:expr1,exprN]. The list contains a list of expressions; one intended for each invocation of the stubbed method. If the stub is executed more times than there are expressions in the stub list, the last expression will be used repeatedly.
For example if you want the book method to return 1 on the first invocation, 10 on the second, and 50 on the third, you could specify:
stubs="SeatServiceDelegate.book(List,Customer,int)=sl:1,10,50"
You can use any expression within stub lists, not just values. So if necessary you could call different methods or pass different argument values depending on a position in the invocation sequence.
5.2.3.4. Throwing an Exception
You can specify that a subbed method throws any given exception. To do this, use ^= rather than =:
matchExpression ^= exceptionResultExpression
The exceptionResultExpression is any expression that returns a Throwable object. The Throwable needs to be of a type supported by the stubbed method’s signature, i.e. it needs to be on the list of exceptions declared in the stubbed method’s throws clause.
In the following example, we’re testing the scenario where the cancelSeat method throws a RemoteException:
@Exemplar(args={"Booking/i1"}, expectexception="TechnicalException", expect="retval.getCause().getMessage().equals(‘test msg’)", stubs="SeatServiceDelegate.cancelSeat^=new java/net/RemoteException('test msg')") public void cancelBooking(Booking booking) throws SeatNotFoundException { ... try { List seatIDs = booking.getSeatIDs(); for (int seatID : seatIDs) { seatServiceDelegate.cancelSeat(seatID); } booking.setCancelled(); } catch (NamingException e) { throw new TechnicalException(e); } catch (RemoteException e) { throw new TechnicalException(e); } ... }
public class SeatServiceDelegate { InitialContext ctx; ... public void cancelSeat(int seatID) throws NamingException, RemoteException, SeatNotFoundException { // Access remote service to cancel the given seat SeatService service = (SeatService) ctx.lookup("SeatService"); service.cancel(seatID); } ... }