meeting -*- Outline -*- * Unit testing with JUnit ** what is unit testing? ------------------------------------------ WHAT IS UNIT TESTING? def: *testing* is the process of showing that a program works for certain inputs def: a *unit* is a module, or small set of modules. In Java, a unit is a class or interface, or a small set of them. E.g., an interface and 3 classes that implement it. def: *unit testing* is testing of a unit. ------------------------------------------ the inputs in testing are supposed to be representative, and thus give some confidence in the whole program's correctness Q: Do you get more confidence by running more test data? Sometimes ** goals and motivation Unit testing is important for teams... ------------------------------------------ WHY UNIT TESTING? Code isn't right if it's not tested Practical: - most programmers rely on testing - e.g., Microsoft has 1 tester per developer - you could get work as a tester Divide and conquer: - split system into units - debug units individually - narrow down places where bugs can be - don't want to chase down bugs in other units Support regression testing: - so can make changes to lots of code and know if you broke something - can make big changes with confidence ------------------------------------------ ** how to do unit testing Ideally unit testing is done in isolation from other units. But in practice, it's more economical to rely on already tested units. ------------------------------------------ HOW TO DO UNIT TESTING Build system in layers - start with classes that don't depend on others - continue testing building on already tested classes Benefit: - avoids having to write stubs - when testing a module, ones it depends on are reliable ------------------------------------------ You can also apply the same layering idea to methods in a class. Q: How does low coupling help testing? more choices of what to test when Q: How does high coupling hurt it? have to test some classes together, or write stubs *** example without JUnit ------------------------------------------ PROGRAM TO TEST public class ISqrt { /** Return an integer approximation to the square root of y. */ public static int isqrt(int y) { int guess = 1; while (guess * guess < y) { guess++; } return guess; } } ------------------------------------------ The conventional way to test this would be as follows... ------------------------------------------ CONVENTIONAL TESTING /** Test ISqrt. */ public class TestISqrtNoJUnit { /** Run the tests. */ public static void main(String[] args) { printTestResult(0); printTestResult(1); printTestResult(2); printTestResult(3); printTestResult(4); printTestResult(7); printTestResult(9); printTestResult(100); } private static void printTestResult(int arg) { System.out.print("isqrt("); System.out.print(arg + ") ==> "); System.out.println( ISqrt.isqrt(arg)); } } ------------------------------------------ ------------------------------------------ CONVENTIONAL TEST OUTPUT isqrt(0) ==> 1 isqrt(1) ==> 1 isqrt(2) ==> 2 isqrt(3) ==> 2 isqrt(4) ==> 2 isqrt(7) ==> 3 isqrt(9) ==> 3 isqrt(100) ==> 10 ------------------------------------------ Q: What does this say about the code? Is it right? Q: What's the problem with this kind of test output? *** example with JUnit ------------------------------------------ import junit.framework.*; import junit.textui.*; /** Test ISqrt. */ public class TestISqrt extends TestCase { /** Run the tests. */ public static void main(String[] args) { TestRunner.run(suite()); } /** Test isqrt. */ public void testIsqrt() { // line 26 below assertEquals(0, ISqrt.isqrt(0)); assertEquals(1, ISqrt.isqrt(1)); assertEquals(1, ISqrt.isqrt(2)); assertEquals(1, ISqrt.isqrt(3)); assertEquals(2, ISqrt.isqrt(4)); assertEquals(2, ISqrt.isqrt(7)); assertEquals(3, ISqrt.isqrt(9)); assertEquals(10, ISqrt.isqrt(100)); } /** Returns the test suite for this test class. */ public static Test suite() { return new TestSuite(TestISqrt.class); } } ------------------------------------------ Explain all of this, although can leave the suite stuff for later. ------------------------------------------ COMPILATION AND OUTPUT (formatted) $ javac ISqrt.java TestISqrt.java $ java TestISqrt .F Time: 0.03 There was 1 failure: 1) testIsqrt(TestISqrt) junit.framework.AssertionFailedError: expected:<0> but was:<1> at TestISqrt.testIsqrt (TestISqrt.java:26) at sun.reflect.NativeMeth... at sun.reflect.NativeMeth... at sun.reflect.Delegating... at TestISqrt.main (TestISqrt.java:15) FAILURES!!! Tests run: 1, Failures: 1, Errors: 0 ------------------------------------------ Q: Is this better? Why? *** exercise for students have them supply test data and some of the test oracle for a method ------------------------------------------ FOR YOU TO DO Write a JUnit test class for testing public class ForYou { /** Return the minimum of x and y. */ public static int min(int x, int y) { ... } } === By filling in the following: === import junit.framework.*; import junit.textui.*; /** Test ForYou. */ public class TestForYou extends TestCase { /** Test min. */ public void testMin() { } /* ... rest as before ... */ } ------------------------------------------ Give them 2 minutes. ** using the JUnit framework *** definitions To discuss JUnit in more detail, we need some terms **** test case, success and failure ------------------------------------------ SOME TERMINOLOGY def: A *test case* for a method M is a pair (o, args) where o is not null and M can be sent to o, args is a tuple of arguments that can be passed to M def: A test case, (o, args), for M *succeeds* iff it o.M(args) behaves as expected. def: A test case, (o, args), for M *fails* iff it does not behave as expected. ------------------------------------------ Q: Why should o not be null? Q: If M has a bug that is revealed by a test case, does that test case for M succeed or fail? Some (particularly testers) reverse the sense of the terms "success" and "failure". We follow JUnit's conventions here. JUnit says a test is in error if it encounters an unexpected exception. **** test code terms ------------------------------------------ PARTS OF TEST CODE def: The *test fixture* is the set of variables used in testing. def: The *test driver* is the class that runs the tests def: The *test oracle* for a test case is the code that decides success or failure for that test case. ------------------------------------------ Q: What in the code we saw so far was the test driver? The oracle? Q: What difference is there between JUnit testing and non JUnit testing in what we saw before? no oracle in the code that wasn't JUnit *** basic usage ------------------------------------------ BASIC USAGE OF JUNIT (1) To test a type T: 1. Write a class like: import junit.framework.*; import junit.textui.*; /** Test of class T. */ public class TestT extends TestCase { /** Run the tests. */ public static void main(String[] args) { TestRunner.run(suite()); } /** Returns the test suite for this test class. */ public static Test suite() { return new TestSuite(TestT.class); } } ------------------------------------------ write 1 test method per method of T, or per use case, etc. call them all "testM" where M is a method name. ------------------------------------------ BASIC USAGE OF JUNIT (2) 2. Compile T.java and TestT.java $ javac T.java TestT.java 3. Run the JUnit graphical user interface on TestT $ java junit.swingui.TestRunner TestT OR Run the text interface (good from makefiles) $ java TestT 4. Look at the failures and errors ------------------------------------------ Eclipse also has direct support for JUnit tests *** extended example from StickSync ------------------------------------------ package sticksync; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.util.Date; import junit.framework.TestCase; /** * This is a JUnit test case for the Location class. * @author Curtis Clifton * @author Gary T. Leavens * @version $Revision: 1.6 $ */ public class TestLocation extends TestCase { private File[] testFiles; private Location[] testLocs; public TestLocation(String name) { super(name); } /** Run the tests. */ public static void main(String[] args) { junit.textui.TestRunner.run(suite()); } /** Returns the test suite for this test class. */ public static junit.framework.Test suite() { return new junit.framework.TestSuite(TestLocation.class); } public void testConstructors() { for (int i=0; i < testFiles.length; i++) { Location l1 = new Location(testFiles[i].getPath()); assertEquals(l1.getPath(),testLocs[i].getPath()); } } public void testReadable() { for (int i=0; i < testLocs.length; i++) { assertTrue(testLocs[i].readable()); } } public void testWriteable() { for (int i=0; i < testLocs.length; i++) { assertTrue(testLocs[i].writeable()); testFiles[i].setReadOnly(); assertTrue(!testLocs[i].writeable()); } } public void testCopyTo() throws IOException { String orig0 = testLocs[0].contentsAsString(); String orig1 = testLocs[1].contentsAsString(); assertTrue(!orig0.equals(orig1)); Date d = testLocs[0].copyTo(testLocs[1]); assertEquals(orig0, testLocs[1].contentsAsString()); assertEquals(d,testLocs[0].modTime()); assertEquals(d,testLocs[1].modTime()); } public void testAbsorbChangesFrom() throws IOException { // !E2! This test assumes merge will fail boolean result = testLocs[0].absorbChangesFrom(testLocs[1]); assertTrue(!result); assertEquals(testLocs[1].contentsAsString(), testLocs[0].getNearbyLocation().contentsAsString()); } protected void setUp() throws Exception { testFiles = configureTestFiles(2); testLocs = new Location[testFiles.length]; for (int i=0; i but was: at ...TestLocation.testCopyTo( TestLocation.java:76) at sun.reflect.NativeMethod... at sun.reflect.NativeMethod... at sun.reflect.Delegating... at ...TestLocation.main( TestLocation.java:38) FAILURES!!! Tests run: 5, Failures: 1, Errors: 0 ------------------------------------------ Explain how to read this, and where to find the error in the source ------------------------------------------ class Location { /* ... */ /** Copies this location's file to the given location's file. */ public Date copyTo(Location loc) throws IOException { InputStream source = new BufferedInputStream( new FileInputStream(getFile())); OutputStream dest = new BufferedOutputStream( new FileOutputStream( loc.getFile())); byte[] buf = new byte[256]; int count = 0; while ((count = source.read(buf)) != -1) { dest.write(buf,0,count); } source.close(); dest.close(); getFile().setLastModified( loc.modTime().getTime()); return new Date(); } } ------------------------------------------ The problem is on the last line, which should return the file's mod time, not the current time. ** other aspects of JUnit *** naming conventions ------------------------------------------ NAMING CONVENTIONS Test methods start with "test" e.g., testCopyTo, testIsqrt Test classes start with "Test" e.g., TestLocation ------------------------------------------ The method conventions are important if you count on reflection *** test suites ------------------------------------------ TEST SUITES Organize tests into a larger test. Help with automation of testing. See the JUnit documentation for details. ------------------------------------------