From: Steve F. <sm...@us...> - 2002-08-05 01:27:15
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs In directory usw-pr-cvs1:/tmp/cvs-serv8393/doc/xdocs Added Files: quickmock.xml Log Message: Added Nat's paper --- NEW FILE: quickmock.xml --- <?xml version='1.0'?> <!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN" "http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd" [ <!ENTITY QM "Quick Mock"> ]> <article> <title>A Quick Run Through Through &QM;</title> <chapterinfo> <authorblurb> <author><personname> <honorific>Dr.</honorific> <firstname>Nat</firstname> <surname>Pryce</surname> </personname></author> <email>nat...@b1...</email> </authorblurb> <copyright> <year>2002</year> <holder><forename>Nat</forename> <surname>Pryce</surname></holder> </copyright> </chapterinfo> <section><title>Introduction</title> <para> QuickMock is a <ulink url="http://java.sun.com">Java</ulink> library that helps the developer build <ulink url="http://www.mockobjects.com">mock objects</ulink> for use in unit tests. It uses JUnit and the Java Reflection API, particularly the dynamic <classname>java.lang.reflect.Proxy</classname> class introduced in JDK 1.3. </para> <para> There are several existing tools and libraries for building mock objects. Why another? What does &QM; bring the the party that other tools do not? Here's a short list of features that drove the design of &QM;: </para> <variablelist> <varlistentry> <term>In-line Definition</term> <listitem> <para> Mocked behaviour and expectations can be defined inline in your JUnit tests, right where they are used. You don't have to write additional classes, or complicate your build process by generating mock classes from your interfaces. </para> </listitem> </varlistentry> <varlistentry> <term>Simple API</term> <listitem> <para> The &QM; API consists of one class. There is no need to learn a large class library or write a library of Mock classes for each API that you want to test against. Java Reflection does all that work for you.</para> </listitem> </varlistentry> <varlistentry> <term>Well Known Syntax</term> <listitem> <para> Expectations are defined using Java expressions and JUnit assertions, rather than by composing expectation objects into what is, in effect, the abstract syntax tree of an interpreted little-language.</para> </listitem> </varlistentry> <varlistentry> <term>Works Well with IDEs and Refactoring Tools</term> <listitem> <para> Expectations are defined by implementing methods of the interface to be mocked. A base expectation class with no behaviour can be generated in any IDE by automatically stubbing out the methods of the interface. Automatic refactorings applied to the mocked interface, such as renaming methods, interfaces or classes, will propagate through to the expectation objects in your unit tests.</para> </listitem> </varlistentry> <varlistentry> <term>Smooth Path To Reusable Abstractions</term> <listitem> <para> Although you can define mocks inline in your JUnit tests, &QM; does not force you to do so. You can refactor mocked behaviour onto reusable abstractions in a number of increasingly elaborate ways, starting by defining higher level assertions in your test classes, to defining basic preconditions in your expectation class, all the way to deriving custom mock classes from the <classname>Mock</classname> class.</para> </listitem> </varlistentry> <varlistentry> <term>Support for Java Idioms</term> <listitem> <para> &QM; supports Java Bean properties and Command/Query separation. This support simplifies the mocking of many interfaces that would otherwise require hand coding.</para> </listitem> </varlistentry> </variablelist> <para> Taking off the rose tinted spectacles for a moment, there must be some drawbacks to using &QM;. Drawbacks I have experienced include: <itemizedlist> <listitem> <para>You can only mock the behaviour of interfaces. If you want to mock the behaviour of a class (<classname>java.util.Random</classname>, for example) you will have to write it by hand or use another mock objects library.</para> </listitem> <listitem> <para>Methods defined by the Object class, such as <methodname>toString</methodname>, <methodname>equals</methodname> or <methodname>hashCode</methodname>, cannot be mocked up by the Reflection API. If you want to mock the behaviour of these methods you have to write a mock class by deriving from <classname>Mock</classname>. </para> </listitem> </itemizedlist> </para> <para> Hopefully you now have an idea of what &QM; is for, and why you might want to use it. Read on to see what &QM; looks like in action... </para> </section> <section><title>Creating Mock Objects</title> <para> The examples in this chapter assume you know how to write Java Servlets. If you don't, browse the tutorial and documentation on <ulink url="http://java.sun.com">Sun's Java site</ulink>. The Servlet API is very simple, but writing a Servlet exercises every aspect of the &QM; library. </para> <para> In this example we are writing a servlet that implements a simple phone book. The user will pass a name to the servlet as a parameter in the URL, and the servlet will return a page containing the phone number corresponding to that name, or an error message. </para> <para> The first thing we need to do is write a test for the <methodname>doGet</methodname> method of the servlet, and to do that we need to mock up those objects in the servlent's environment that are passed as arguments to <methodname>doGet</methodname>, the request and the response. Using &QM;, this is very easy: just create two instances of the <classname>Mock</classname> class. We will give them names so that they are properly identified in error messages. </para> <programlisting>import com.b13media.quickmock.Mock; class PhonebookServletTest extends junit.framework.TestCase { public PhonebookServletTest( String test ) { super(test); } public void testDoAdd() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); <emphasis>Mock request = new Mock("Request"); Mock response = new Mock("Response");</emphasis> [...] } } </programlisting> <para> Ok, we've created the mock request and response, but they are instances of <classname>Mock</classname> and we have to pass them to the servlet as instances of <classname>HTTPServletRequest</classname> and <classname>HTTPServletResponse</classname>. To do this, we ask the mocks to dynamically create those interfaces by calling <methodname>createInterface</methodname>; the mocks will then provide the implementation of the interfaces that they create. There are several overloaded versions of the <methodname>createInterface</methodname> method, but the simplest usually suffices. It takes a reference to the <classname>Class</classname> of the interface we want to mock, and returns an <classname>Object</classname> reference of an instance of the interface that delegates method calls to the mock that created it. The returned reference must be cast to the interface type that was created because of Java's static type system but the cast will always succeed. </para> <programlisting>public void testDoAdd() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); Mock request = new Mock("Request"); Mock response = new Mock("Response"); [...] <emphasis>servlet.doGet( (HTTPServletRequest)request.createInterface(HTTPServletRequest.class), (HTTPServletResponse)response.createInterface(HTTPServletResponse.class) );</emphasis> [...] } </programlisting> <tip> You can call <methodname>createInterface</methodname> on a <classname>Mock</classname> instance multiple times or pass it an array of multiple interface classes to create. The mock object will act as the implementation of all those interfaces. </tip> </section> <section><title>Defining Expectations</title> <para> Now we need to set up our test environment to test expectations and implement stub behaviour. We do this by telling the mock objects what method calls to expect. Our servlet will query the name parameter from the request object, and then set the content type of the response before getting an a <classname>PrintWriter</classname> to write the response body. </para> <para> Let's start by mocking the request. Our servlet will query the name to look up by calling the request's <methodname>getParameter</methodname> method. We have to set up an expectation of that call by calling the <methodname>expect</methodname> method on the mock request and passing it a "call expectation" object. A call expectation is an object of a class that implements a single method of an interface. That method identifies the method that is expected to be called, checks the parameters against expected values, and returns a canned result. </para> <para> The easiest way to define an expectation is as an anonymous inner class because the definition of the expectation is inline with the unit test, alongside its use. However, if an interface declares more than one method, the compiler will not let us implement just one method in an inner class so we have to declare a concrete class that has stub implementations of all the methods of the interface. &QM; will never actually call any of those stubs so we don't care how they are implemented and can therefore generate the stub class automatically in our IDE: </para> <programlisting>class HTTPServletRequestExpectation implements HTTPServletRequest { [...] } </programlisting> <tip> Make your unit tests easy to read by using a regular and descriptive naming convention for these stub classes. For example, we name them all by appending "Expectation" to the name of the stubbed out interface. </tip> <para> We can now create anonymous subclasses of <classname>HTTPServletRequestExpectation</classname> in our unit test to define the expected calls to the request object: </para> <programlisting>public void testLookupNumber() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); Mock request = new Mock("Request"); Mock response = new Mock("Response"); <emphasis>request.expect( new HTTPServletRequestExpectation() { public String getParameter( String key ) { assertEquals( "expect to get the name parameter", "name", key ); return "Elvis Presley"; } } );</emphasis> [...] servlet.doGet( (HTTPServletRequest)request.createInterface(HTTPServletRequest.class), (HTTPServletResponse)response.createInterface(HTTPServletResponse.class) ); [...] } </programlisting> <para> The expectation will assert that when the servlet calls <methodname>getParameter</methodname> it asks for the "name" parameter. The mock request will fail if the servlet calls some other method instead of <methodname>getParameter</methodname>. It does not, however, assert that the servlet actually does call the <methodname>getParameter</methodname> method. To assert that all the expected calls have been made to the mock objects our test must call their <methodname>verify</methodname> methods: </para> <programlisting>public void testLookupNumber() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); Mock request = new Mock("Request"); Mock response = new Mock("Response"); <emphasis>request.expect( new HTTPServletRequestExpectation() { public String getParameter( String key ) { assertEquals( "expect to get the name parameter", "name", key ); return "Elvis Presley"; } } );</emphasis> [...] servlet.doGet( (HTTPServletRequest)request.createInterface(HTTPServletRequest.class), (HTTPServletResponse)response.createInterface(HTTPServletResponse.class) ); [...] <emphasis>request.verify(); response.verify();</emphasis> } </programlisting> <para> Now we have to define the expected calls to the response object. Again, we use our IDE to generate a <classname>HTTPServletResponseExpectation</classname> class, anonymous subclasses of which will define expected calls to methods on the <classname>HTTPServletResponse</classname> interface. The first thing our servlet will do to the response is set the MIME type of the generated content: </para> <programlisting>public void testLookupNumber() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); Mock request = new Mock("Request"); Mock response = new Mock("Response"); request.expect( new HTTPServletRequestExpectation() { public String getParameter( String key ) { assertEquals( "expect to get the name parameter", "name", key ); return "Elvis Presley"; } } ); <emphasis>response.expect( new HTTPServletResponseExpectation() { public void setContentType( String type ) { assertEquals( "expected HTML type", "text/html", type ); } } );</emphasis> [...] servlet.doGet( (HTTPServletRequest)request.createInterface(HTTPServletRequest.class), (HTTPServletResponse)response.createInterface(HTTPServletResponse.class) ); [...] request.verify(); response.verify(); } </programlisting> <para> Now we have to mock writing the content into the response object. The response returns a <classname>PrintWriter</classname>. Do we have to mock that as well? No. We don't really care about the exact format of the generated content, only that it contains the expected phone number somewhere within it. So rather than checking the output data as it is written, which will tie our tests too closely to presentation details and be complicated to implement, we can can create a <classname>StringWriter</classname> in our test, return a <classname>PrintWriter</classname> that decorates it from our mocked implementation of <methodname>getWriter()</methodname>, and then search for the expected phone number at the end of the test. </para> <programlisting>public void testLookupNumber() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); Mock request = new Mock("Request"); Mock response = new Mock("Response"); <emphasis>final StringWriter response_body = new StringWriter(); String expected_number = [...];</emphasis> request.expect( new HTTPServletRequestExpectation() { public String getParameter( String key ) { assertEquals( "expect to get the name parameter", "name", key ); return "Elvis Presley"; } } ); response.expect( new HTTPServletResponseExpectation() { public void setContentType( String type ) { assertEquals( "expected HTML type", "text/html", type ); } } ); <emphasis>response.expect( new HTTPServletResponseExpectation() { public PrintWriter getWriter() { return new PrintWriter(response_body); } } );</emphasis> servlet.doGet( (HTTPServletRequest)request.createInterface(HTTPServletRequest.class), (HTTPServletResponse)response.createInterface(HTTPServletResponse.class) ); <emphasis>assertTrue( "response body contains expected phone number", response_body.toString().indexOf(expected_number) >= 0 );</emphasis> request.verify(); response.verify(); } </programlisting> <tip> You must declare as final any local variables that are accessed by mocked calls. Unfortunately that means that mocked methods cannot change the values of those variables. See the section about Java Bean Properties for a way to alleviate this drawback. </tip> <para> So, there we are, our first test is finished. Now we just have to actually <emphasis>write</emphasis> the <classname>PhonebookServlet</classname> class. We'll leave that as an excercise for the reader! </para> </section> <section><title>Command-Query Separation</title> <para> Our first test works, but is rather "brittle" because of how we declared the expected call to <methodname>getParameter</methodname>. The <methodname>getParameter</methodname> method doesn't change the state of the response object, so it doesn't actually matter how many times the servlet calls it. However, we have declared that it will be called exactly once. As we extend our servlet class we might change the implementation of <methodname>doGet</methodname> to get the name property more than once and that would break our test. We have exposed internal implementation details in a test of the class' public API. </para> <para> Let's review why we are using Mock objects. We want mock the implementation of other objects in the environment of the object under test so that they returned canned results and so that we can check that the object under test modifies its environment in the way we expect. So our tests should check the expectations of method calls that query the environment (queries) differently from those that modify the environment (commands). </para> <para> &QM; supports Command-Query Separation by letting a unit test declare expected queries. A query is defined just like the method expectations we have seen so far, but &QM; allows a query to be called any number of times, or even not call it at all. Let's change our test to specify that <methodname>getParameter</methodname> is a query: </para> <programlisting>public void testLookupNumber() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); Mock request = new Mock("Request"); Mock response = new Mock("Response"); final StringWriter response_body = new StringWriter(); String expected_number = [...]; <emphasis>request.query( new HTTPServletRequestExpectation() { public String getParameter( String key ) { assertEquals( "expect to get the name parameter", "name", key ); return "Elvis Presley"; } } );</emphasis> response.expect( new HTTPServletResponseExpectation() { public void setContentType( String type ) { assertEquals( "expected HTML type", "text/html", type ); } } ); response.expect( new HTTPServletResponseExpectation() { public PrintWriter getWriter() { return new PrintWriter(response_body); } } ); servlet.doGet( (HTTPServletRequest)request.createInterface(HTTPServletRequest.class), (HTTPServletResponse)response.createInterface(HTTPServletResponse.class) ); assertTrue( "response body contains expected phone number", response_body.toString().indexOf(expected_number) >= 0 ); request.verify(); response.verify(); } </programlisting> <para> The mocked implementation of a query can be changed dynamically during a test. This allows you to mock an object in which a command affects the result of a subsequent query by changing the mocked query within the mocked implementation of the command. However, doing this is a sign that your test might be becoming too large, testing too much in one go, and requires refactoring. Or, you might be trying to mock a Java Bean property; read the next section to find out how to do that. </para> </section> <section><title>Support For Java Bean Properties</title> <para> Bean properties are widely used in Java APIs so &QM; provides specific support for this idiom. Properties don't need explicit expectations. %QM; traps calls to property setters and getters and manages the storage of property values. A mock object handles calls to property setters by storing the new property value, and handles property getters by returning a previously stored value or reporting a test failure if no value is available. </para> <para> Tests can explicitly declare property expectations to check preconditions of the getter and setter methods and to define the initial value of the property. </para> <para> Can test and assert that a property is set on a mock object. </para> <para> Next expectation overrides properties. </para> </section> <section><title>Abstracting Common Expectations</title> <para> Expectations are inner classes, so can abstract common expectation code into assertions in the test class. </para> <para> Can define common preconditions in the stub methods in the expectation class. </para> </section> <section><title>Refactoring our Application Classes</title> <para> </para> <programlisting>public void testLookupNumber() throws ServletException, IOException { PhonebookServlet servlet = new PhonebookServlet(); Mock phonebook = new Mock("Phonebook"); Mock request = new Mock("Request"); Mock response = new Mock("Response"); final StringWriter response_body = new StringWriter(); <emphasis>final String requested_name = [...]; final String requested_number = [...]; phonebook.query( new PhonebookExpectation() { public String lookup( String name ) { assertEquals( requested_name, name ); return requested_number; } } ); request.query( new HTTPServletRequestExpectation() { public String getParameter( String key ) { assertEquals( "expect to get the name parameter", "name", key ); return requested_name; } } );</emphasis> response.expect( new HTTPServletResponseExpectation() { public void setContentType( String type ) { assertEquals( "expected HTML type", "text/html", type ); } } ); response.expect( new HTTPServletResponseExpectation() { public PrintWriter getWriter() { return new PrintWriter(response_body); } } ); <emphasis>servlet.setPhonebook( (Phonebook)phonebook.createInterface(Phonebook.class) );</emphasis> servlet.doGet( (HTTPServletRequest)request.createInterface(HTTPServletRequest.class), (HTTPServletResponse)response.createInterface(HTTPServletResponse.class) ); assertTrue( "response body contains expected phone number", response_body.toString().indexOf(expected_number) >= 0 ); request.verify(); response.verify(); } </programlisting> </section> <section><title>Deriving Custom Mock Classes</title> </section> <para> Can derive custom mock objects from <classname>Mock</classname> and mix hand coded mock behaviour with expectation objects. </para> <section><title>Summary</title> </section> </article> |