480 likes | 617 Views
Testing all kinds of Java things using JUnit and its extensions. Kalvis Apsitis. What are “Programmer Tests”?. Programmer Testing is the testing performed by a developer with the goal of verifying the correct functioning of his/her own code
E N D
Testing all kinds of Java things using JUnit and its extensions Kalvis Apsitis
What are “Programmer Tests”? • Programmer Testing is the testing performed by a developer with the goal of verifying the correct functioning of his/her own code • Programmer Tests are automated unit tests that exercise program units such as classes and methods , not the complete features or requirements supported by the software as a whole • Important distinction: if it’s not automated, it’s not really programmer testing – that’s exploratory testing.
Safety Net • Having a good test coverage with an automated unit test harness… • prevents a system from becoming legacy(*), and • enables effective refactoring, because it also prevents regression • Metaphor: Mold • Michael Feathers defines “legacy code” as code without tests
Michael Feathers (2004) • I don't care how good you think your design is. If I can't walk in and write a test for an arbitrary method of yours in five minutes its not as good as you think it is, and whether you know it or not, you're paying a price for it: • You think your design is good? Pick a class, any class, and try to instantiate it in a test harness. • Can you make this class work outside the application? • Can you get it to the point where you can tinker with it, in real time, build it alone in less than a second, and add tests to find out what it really does in certain situations.
Available tools for Java • JUnit (www.junit.org) • The de facto Java unit testing framework • Extremely simple, suitable for unit/component testing • Plenty of extensions for J2EE, for example • TestNG (testng.org) • Very close to JUnit but not quite • Can configure test groups/suites in a more flexible way (it is usable for some integration testing)
JUnit Concepts • Test suite: Executable row of several Test cases • Test case: Java class containing test methods • Test fixture: the initial state of a Test Case, consisting of the member variables initialized through a method annotated with @Before. E.g. @Before public void setUp() { … } • Test method: a no-argument method of a TestCase class annotated with @Test (represents a single logical test) For example: @Test public void someMethod() { … } ============ Only things to do in “@Test testA(){...}” methods is business logic + assertions. Test fixture contains everything else needed to run the tests (it is initialized in "@Before setUp(){...}").
TestCase lifecycle @Before public void setUp() {...} gets called once before each test method is executed @After public void tearDown() {...} gets called once after each test method has been executed – regardless of whether the test passed or failed (or threw some other exception). • Each test method gets called exactly once in some order (test methods should be isolated, so the order does not matter). • The same instance of a TestCase may or may not be used for executing the tests – don’t depend on the constructor, use setUp() for setup!
Suite Example import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses(value = {TestCase1.class, TestCase2.class}) public class SomeSuite { // any code; // may contain TestCase1, TestCase2 // as inner classes, if their fixtures have much // in common }
Simple Annotations in a TestCase import org.junit.*; public class TestCase1 { @Before protected void setUp() {super.setUp();} @After protected void tearDown() {super.tearDown();} @Test public void methodOne() { ... } @Test(expected = RuntimeException.class) public void testSomeException() { ... } @Ignore(value = "Test method not ready") @Test public void ignoredTestCase() { ... } }
Simple TestCase (3) import org.junit.*; public class TestCalculator { private Calculator calculator; protected void setUp() { super.setUp(); calculator = new Calculator(); } @Test public void addingPositiveIntegers() { int expected = 5; int actual = calculator.add(2, 3); assertEquals("2 + 3 should be 5", expected, actual); } }
Test anything that could fail import org.junit.*; public class TestCalculator { private Calculator calculator; @Before protected void setUp() { … } @After protected void tearDown() { … } @Test public void addingPositiveIntegers() { … } @Test public void addingNegativeIntegers() { … } @Test public void addingZeroes() { … } @Test public void addingPositiveToNegative() { … } } How many method calls are executed?
Test Granularity • Each unit test should check one specific piece of functionality. Do not combine multiple, unrelated tests into a single testXXX( ) method. • If the first assertEquals( ) in some method fails, the remainder is not executed. You won't know if the other (unrelated) asserts are functional. • One method may contain several asserts, if they are related, i.e. if the failure of one assert would cause failures in other asserts anyway.
Exercise 1:Write a TestCase • Task: Write a JUnit TestCase for the Template class, place it under sem02-demo/src/main/java/exercise1/TemplateTest.java • There’s a bug in Template class. Try to find it!
Simple TestSuite import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses(value = {CalculatorIntegerTest.class, CalculatorFloatingPointTest.class, CalculatorLogarithmsTest.class }) public class CalculatorTestSuite { // Can leave empty }
Exercise 2:Write a TestSuite • Task: • Write a JUnit TestSuite exercise2.AllTests that collects all the TestCase classes from sem02_demo/src/test/java/exercise2.
What if I need to do something expensive in setUp()? • If you need to perform some kind of one-time setup before running the test methods of a TestCase (and can’t/won’t replace the expensive stuff with a mock implementation), use @BeforeClass annotation: public class Example { @BeforeClass public static void onlyOnce() { } @Before public void beforeEachTest() {} @Test public void one() { ... } @Test public void two() { ... } }
Example of @BeforeClass public class InterestCalculatorTestWithTestSetup { private static double interestRate, loanAmount; private static int loanDuration; public static Test suite() { TestSuite suite = new TestSuite(InterestCalculatorTestWithTestSetup.class); TestSetup wrapper = new TestSetup(suite) { public void setUp() throws IOException { ResourceBundle bundle = ResourceBundler.getBundle( InterestCalculatorTestWithTestSetup.class.getName()); inrerestRate = Double.parseDouble(bundle.getString(“interest.rate”)); loanAmount = Double.parseDouble(bundle.getString(“loan.amount”)); loanDuration = Interest.parseInt(bundle.getString(“loan.duration”)); } }; return wrapper; } public void testInterestCalculation() { … } }
Assertions • We already saw one assertion in action, the assertEquals() method • There are a bunch others – take a look at the Javadocs for org.junit.Assert • The most often used assertions –assertEquals(), assertNull(), assertSame(), assertTrue() and their opposites – are enough for most situations and the rest you can easily write on your own. • Assert.fail() is used, if control should not reach that line in the test - this makes the test method to fail.
Testing Expected Exceptions @Test public void method1() { try { Template t = new Template( "My ${f\\oo template" ); fail("Should NOT allow unterminated variables in " + t); } catch (IllegalArgumentException expected) {} } @Test(expected=IllegalArgumentException.class) public void method2() { new Template( "This is my ${f\\oo template" ); } First approach is longer, but allows more precise control, where exactly the test method fails.
What happens when an assertion fails? • The failed assertions create a org.junit.runner.notification.Failure when they fail. • JUnit tags the test as “failure” for later reference. • If a test throws any other exception (e.g. NullPointerException), JUnit again catches these but tags the test as “error” instead of “failure”.
Mock Objects • At some point, you will face a problem where the class/method you should be testing needs to collaborate with an object that is either difficult to create/obtain or simply sooo slooow that your test takes forever to execute (even a 100ms adds up when you’ve got thousands of tests…). • The solution for this kind of problems is often to use a mock object.
What are Mock Objects? • A mock object is an object that claims to implement an interface but doesn’t really. • There are variations on what the “doesn’t really” part actually means (these variations are called “fake”, “stub” and “mock”) but for now, we’ll just call them all mock objects. • Since there’s nothing like a good example…
Example: Mock Objects public class Item { public float getPriceAfterDiscounts(PricingService ps, DiscountService ds) { float basePrice = ps.getPriceFor(this); float discountedPrice = ds.applyDiscounts(this, basePrice); return discountedPrice; } } public interface PricingService { float getPriceFor(Item item); } public interface DiscountService { float applyDiscounts(Item item, float basePrice); }
How to test that without the real PricingService & DiscountService? public class TestItemUsingMockObjects extends TestCase { private Item item; private PricingService pricingService; private DiscountService discountService; protected void setUp() { item = new Item(); pricingService = new PricingService() { public float getPriceFor(Item item) { return 12.34f; } }; discountService = new DiscountService() { public float applyDiscounts(Item item, float basePrice) { …} }; } public void testCalculateDiscountsOnBasePrice() { assertEquals(10.95f, item.getPriceAfterDiscounts(pricingService, discountService); } }
Static vs. Dynamic Mocks • That example used “static” mock objects – the other school of mock objects is “dynamic” mock objects • Static mock objects fake their own behavior while dynamic mock objects also verify that the class under test collaborated with the mock objects as expected • Let’s see an example to illustrate this behavior-oriented approach to using mock objects (we’ll use the EasyMock framework but there are some alternatives)
Using EasyMock anddynamic mock objects public class TestItemUsingMockObjects extends TestCase { private Item item; private MockControl pricingControl, discountControl; private PricingService pricingService; private DiscountService discountService; protected void setUp() { item = new Item(); pricingControl = MockControl.createControl(PricingService.class); // obtain a “remote control” pricingService = (PricingService) pricingControl.getMock();// let EasyMock create the mock object pricingService.getPriceFor(item);// specify expected method call to our mock PricingService pricingControl.setReturnValue(12.34f);// fake the behavior by setting return value pricingControl.replay();// switch from recording mode to replay mode // similar setup for DiscountService... } public void testCalculateDiscountsOnBasePrice() { assertEquals(10.95f, item.getPriceAfterDiscounts(pricingService, discountService); pricingControl.verify(); // verify expected interactions actually happened discountControl.verify(); // verify expected interactions actually happened } }
Exercise 1-5-3:Using EasyMock • Task: • Complete the test in session-1-5/src/exercise3/TestPricingMachineWithEasyMock.java using EasyMock that does the same as TestPricingMachineWithStaticMockObject.java does using an anonymous class • The source file includes instructions inline in the test method to complete
Unit testing database code:The Problem • The database is a common headache for a beginning unit tester • Long methods • Complex interfaces (javax.sql) – leading to verbose setup code even for relatively simple queries • Need to have the expected data in the database • Slow tests because of the database connection
Unit testing database code:The Solutions • You can… • Use various mock object libraries (MockObjects, EasyMock, JMock) to mock the java.sql.* interfaces and just bite your lip with the lengthy test classes • Switch to using a better framework for persistence (e.g. Hibernate) • Refactor your JDBC framework to not rely on java.sql.* so tightly
Unit testing database code:What it might look like public ProjectConfiguration loadProjectConfiguration(String projectId) throws LocalStorageException { Query query = null; try { query = getQueryBuilder().build("loadProjectConfiguration", Collections.singletonList(projectId)); ResultSet rs = getQueryExecuter().executeQuery(query); if (rs.next()) { return getMapper().makeProjectConfiguration(rs); } } catch (Exception e) { e.printStackTrace(); } finally { if (query != null) { query.closeResources(); } } return null; } The dependencies in this piece of code to the java.sql.* interfaces has been reduced to just one – java.sql.ResultSet. The other, more complex (to mock) interfaces like java.sql.Connection and java.sql.PreparedStatement have been hidden inside the QueryBuilder and Query interfaces. The next step would be to get rid of java.sql.ResultSet as well in favor of a more generic tabular data interface which would make the test code for this class yet another notch cleaner.
Unit testing against a real database • Regardless of how thin you can squeeze your persistence framework, it is good practice to run some tests every now and then against the real database using the real JDBC driver, etc. just to make sure that the thing works end-to-end. • The problems with this eventually come down to one big one: • How to populate test data into the database?
DbUnit • JUnit extension for… • Populating test data into the database before each test execution • Making assertions against the database contents • Most often uses XML files to read the test data (referred to as a “data set”) to be populated but supports also Excel files, a database-driven data set, and enables relatively simple interfaces for implementing your own
DbUnit XML files <?xml version="1.0"?> <dataset> <catalog.coffeebeansproductId="000" coffeeName="Sumatra" price="750"/> <catalog.coffeebeans productId="001" coffeeName="Special Blend" price="825"/> <catalog.coffeebeans productId="002" coffeeName="Colombiano" price="925"/> </dataset> Schema Table Column Value
DbUnit test code public class FindProductsTest extends org.dbunit.DatabaseTestCase { // called by DbUnit protected org.dbunit.database.IDatabaseConnection getConnection() throws Exception { Connection connection = …; // open up a connection to the database return new DatabaseConnection(connection); } // called by DbUnit protected org.dbunit.dataset.IDataSet getDataSet() throws Exception { return new org.dbunit.dataset.xml.FlatXmlDataSet(new File(“FindProductsTest.xml”)); } public void testFindAllProducts() throws Exception { // test your data access class against the real database, // populated with test data from FindProductsTest.xml } }
Testing J2EE web components • There can be a whole bunch of components in a J2EE web application that are more or less dependent on the application server’s container for correct functioning • Examples include • Java Servlets • JavaServer Pages • Custom Taglibs • Servlet Filters
The problem statement • How to test these components without first deploying them to the J2EE web container? • Two solutions: • Use a lightweight web container for testing • Mock out the web container • Let’s see how this works out in practice…
Unit testing Java Servlets:The Servlet being tested public class AddToShopcartServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // obtain the shopping cart (or create one if we don't have one yet) HttpSession session = request.getSession(); Shopcart shopcart = (Shopcart) session.getAttribute("shoppingCart"); if (shopcart == null) { shopcart = new Shopcart(); session.setAttribute("shoppingCart", shopcart); } // figure out what to do and do it String itemId = request.getParameter("itemId"); String quantity = request.getParameter("quantity"); shopcart.addItem(itemId, Integer.parseInt(quantity)); // forward to a JSP for rendering the response RequestDispatcher rd = request.getRequestDispatcher("/showCart.jsp"); rd.forward(request, response); } }
Unit testing Java Servlets:Using the MockObjects lib (1) public class TestAddToShopcartServlet extends TestCase { private boolean addItemWasCalled; public void testAddTwoApples() throws Exception { MockHttpSession session = new MockHttpSession(); session.setupGetAttribute(“shoppingCart”, new Shopcart() { public void addItem(String itemId, int quantity) { addItemWasCalled = true; assertEquals(“apple”, itemId); assertEquals(2, quantity); } }); ... } }
Unit testing Java Servlets:Using the MockObjects lib (2) public class TestAddToShopcartServlet extends TestCase { private boolean addItemWasCalled; public void testAddTwoApples() throws Exception { MockHttpSession session = ...; MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletRequest request = new MockHttpServletRequest(); request.setupAddParameter(“itemId”, “apple”); request.setupAddParameter(“quantity”, “2”); request.setSession(session); request.setExpectedRequestDispatcherURI(“/showCart.jsp”); request.setupGetRequestDispatcher(new MockRequestDispatcher()); ... } }
Unit testing Java Servlets:Using the MockObjects lib (3) public class TestAddToShopcartServlet extends TestCase { private boolean addItemWasCalled; public void testAddTwoApples() throws Exception { MockHttpSession session = ...; MockHttpServletResponse response = ...; MockHttpServletRequest request = ...; AddToShopcartServlet servlet = new AddToShopcartServlet(); servlet.init(); // assuming the servlet doesn’t need initialization servlet.doGet(request, response); assertTrue(addItemWasCalled); } }
Exercise 1-5-4:Testing Servlets using MockObjects • Task: • Write a test using the MockObjects library to verify that the AddToShopcartServlet behaves correctly when adding 2 apples into a shopping cart which already has 5 apples • The file TestAddToShopcartServletUsingMockObjects.java already exists in session-1-5/src/exercise4/ with inline comments as exercise instructions.
Exercise 1-5-5:Testing Servlets using EasyMock • Task: • Write a test using the EasyMock library to verify that the AddToShopcartServlet behaves correctly when adding 2 apples into a shopping cart which already has 5 apples • The file TestAddToShopcartServletUsingEasyMock.java already exists in session-1-5/src/exercise5/ with inline comments as exercise instructions.
What about Servlet Filters? • Yes, the same technique works equally well for Servlet Filters -- You just call filter.doFilter(request, response, chain) instead of servlet.doGet(request, response)…
When using a mock objects library becomes too much work • Consider using a lightweight web container such as the ServletRunner class that comes with HttpUnit • Pros: may result in cleaner test code compared to using regular mock objects libraries when dealing with complex Servlets • Cons: tests take longer to execute because of all the infrastructure involved (still multiple magnitudes faster than deploying on a real container...)
Servlets:Using ServletRunner (1) public class TestAddToShopcartServlet extends BaseServletUnitTestCase { protected void setUp() throws Exception { Map servlets = new HashMap(); servlets.put(“/AddToShopcart”, AddToShopcartServlet.class.getName()); servlets.put(“/showCart.jsp”, FakeServlet.class.getName()); ServletRunner servletRunner = new ServletRunner(generateWebXml(servlets), “/myapp”); ... } private File generateWebXml(Map servletNamesAndClasses, String contextName) { // generate a temporary file that acts as the web.xml for ServletRunner's web container // with <servlet> and <servlet-mapping> elements for the servlets listed in the // 'servletNamesAndClasses' map. } }
Servlets:Using ServletRunner (2) public class TestAddToShopcartServlet extends BaseServletUnitTestCase { private InvocationContext addTwoApplesInvocation; protected void setUp() throws Exception { ServletRunner servletRunner = …; ... WebRequest addTwoApplesRequest = new GetMethodWebRequest(“http://localhost/mmyapp/AddToShopcart”); addTwoApplesRequest.setParameter(“itemId”, “apple”); addTwoApplesRequest.setParameter(“quantity”, “2”); ServletUnitClient client = servletRunner.newClient(); addTwoApplesInvocation = client.newInvocation(addTwoApplesRequest); } }
Servlets:Using ServletRunner (3) public class TestAddToShopcartServlet extends BaseServletUnitTestCase { private InvocationContext addTwoApplesInvocation; public void testAddToExistingNonEmptyShopcart() throws Exception { HttpSession session = addTwoApplesInvocation.getRequest().getSession(); session.setAttribute(“shoppingCart”, new Shopcart() { { addItem(“orange”, 5); addItem(“apple”, 4); } }); new AddToShopcartServlet().service(addTwoApplesInvocation.getRequest(), addTwoApplesInvocation.getRequest()); ... } }
Servlets:Using ServletRunner (4) public class TestAddToShopcartServlet extends BaseServletUnitTestCase { private InvocationContext addTwoApplesInvocation; public void testAddToExistingNonEmptyShopcart() throws Exception { ... Shopcart shopcart = (Shopcart) addTwoApplesInvocation.getRequest() .getSession().getAttribute(“shoppingCart”); assertEquals(“Existing contents of a shopping cart should not be dumped”, 5, shopcart.getQuantity(“orange”)); assertEquals(“The quantity should accumulate as items are added”, 6, shopcart.getQuantity(“apple”)); } }