Download presentation
Presentation is loading. Please wait.
1
Lecture 17 CS
2
Testing Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. --Brian Kernighan
3
Write and Test Your Code Incrementally
Just as computers can only do simple tasks, humans can only think one simple thought at a time You can’t solve a complicated problem all at once. Instead, break it down into many simple problems and solve them one at a time, testing as you go. "divide et impera"
4
Write and Test Your Work Incrementally
We are writing a program in which a store tracks a group of salespeople. We need to be able to list the salespeople by name and year-to-date sales The store is modeled by an object of class Store The staff is modeled by an array list of objects of class Salesperson Store must have a name, an array list of Salesperson objects, ways to get data for these fields, a toString(), and a method to list all the salespeople, using data from the objects. Salesperson must have a lastname, a firstname, an id number, a double for year to date sales, some getters and setters, and a toString() method. It should probably have a constructor that takes the names as parameters.
5
Write and Test Your Work Incrementally
Two approaches: Top-Down: write the high-level class (Store) first Store has a dependency: it needs Salesperson to work correctly. If we write it first, we must “stub out” some of the methods or write simulations, then fill in the details later. This approach is analytical; it takes things apart Bottom-Up: write the low-level class (Salesperson) first, then write Store. Salesperson doesn’t depend on anything else, so we can write everything we know it needs all at once, although we will inevitably later realize we need to add more. This approach is synthetic; it puts things together. However, we still have to analyze the problem down to its basic elements first. Both have strengths and weaknesses Bottom-up is simpler, but top-down matches the way we typically think out the project initially. Either way, try to write the smallest unit of code that you can test. Get it working before you proceed any farther. Repeat until you are done!
6
Top-Down: Write Store First
package store; import java.util.ArrayList; import java.util.List; public class Store { private String name; private List<Salesperson> staff; // we must stub out the Salesperson class for this to work, but it doesn’t need anything beyond a class signature and one set of curly braces. public Store(){ staff = new ArrayList<Salesperson>(); } public void setName(String nameIn) { name = nameIn; public void hireStaffPerson() { // We can't make this work until Salesperson is working, so we just write a stub System.out.println("hireStaffPerson not implemented yet"); public void fireStaffPerson(){ //See note above public String toString() { return name; public String listStaff() { // can't make this work until after we get Salesperson working return "listStaff not implemented yet";
7
Top-Down: Write Store First
package store; public class StoreDriver { public static void main(String[] args) { Store store = new Store(); store.setName("Viking Furniture"); store.hireStaffPerson(); System.out.println(store.toString()); System.out.println(store.listStaff()); }
8
Test Your Work Incrementally
Let’t test it: Output: hireStaffPerson not implemented yet Viking Furniture listStaff not implemented yet So far, Store is working as intended. Next, we implement Salesperson, the update the methods in Store to use actual Salesperson data.
9
Bottom-Up: Write Salesperson First
package store; public class Salesperson { private String lastName; private String firstName; private double ytdSales; public Salesperson(String lastIn, String firstIn){ lastName = lastIn; firstName = firstIn; ytdSales = 0; } public void recordSale(double saleAmount){ ytdSales += saleAmount; public String toString() { return lastName + ", " + firstName + " YTD Sales: " + ytdSales; public String getName(){ return lastName + ", " + firstName; public void setLastname(String lastName) { this.lastName = lastName; public void setFirstName(String firstName) { this.firstName = firstName; public double getYtdSales() { return ytdSales; public void setYtdSales(double ytdSales) { this.ytdSales = ytdSales;
10
Bottom-Up: Write Salesperson First
package store; public class StoreDriver { public static void main(String[] args) { String testFirstName = "Joe"; String testLastName = "Smith"; Salesperson p = new Salesperson(testLastName, testFirstName); String testName = testLastName + ", " + testFirstName; if (!(p.getName().equals(testName))) System.out.println("name error"); if (Math.abs(p.getYtdSales() >.001)) System.out.println("Sales error"); double testSaleAmount = ; p.recordSale(testSaleAmount); if (Math.abs(p.getYtdSales() - (0+testSaleAmount)) > .001) System.out.println(p.toString()); }
11
Bottom-Up: Write Salesperson First
Once Salesperson is working correctly, then proceed to write Store
12
Testing Finding and correcting errors is the most time-consuming part of programming Testing and debugging always takes much more time than the first pass at writing code You can make this process easier if you are systematic about testing. Test early and often, and test the smallest increments of code that you can in order to isolate errors. This is one more reason to break your code down into many simple methods instead of a few complicated ones.
13
Testing Professional software development organizations have full-time testers whose job is to find the faults in your code They will always find some, but the better your code is when they get it, the longer you will keep your job The earlier errors are caught, the easier it is to correct them and the fewer person-hours are expended on testing and debugging. The first line of testing is done by the programmers themselves.
14
Testing Besides providing plenty of output that you can check, test various conditions that should be true or false. package monsters; public class MonsterAttackDriver { public static void main(String[] args) { Monster dracula; String dName = "Dracula", dNewName = "Bob", dHome = "Transylvania"; dracula = new Vampire(dName, dHome); if(!(dracula.getName().equals(dName) ) System.out.println("Name error"); dracula.setName(dNewName); if(!(dracula.getName().equals(dNewName)) System.out.println("Name error"); if(dracula.getOriginStory() == null) System.out.println("Origin story error"); // etc. Test all public methods }
15
Unit Testing We have used driver and tester classes to test our classes We have also written test methods that look for likely errors Unit testing is a more systematic way to test parts of your applications Test for every likely error you can think of, starting with the simplest and moving to the more complex. Each test asserts that some condition is true or false; the assertions will fail if particular errors occur
16
JUnit Eclipse includes a unit testing framework called JUnit
A test case is a class that contains one or more tests, usually all testing the same target class. Create one or more separate packages in each project for test cases. Tests use assertions of various kinds assertNull(Object o), assertNotNull(Object o) assertEquals(Object o, Object p), assertFalse(boolean) Many others listed here: A test succeeds if the assertion(s) are true when the test is run and fails if one or more are false The object of a test is to assert something that will be true if the class is working correctly but false if some plausible error occurs
17
JUnit Let's start by writing unit tests for the Vampire class, which implements the Monster interface, and for the Crypt class, which is used by Vampire. Test Crypt first, because it can work without Vampire, but Vampire will not work if Crypt is broken
18
Monster getLocation() is new in this version of Monster
package monsters; public interface Monster { public void setName(String name); public String getName(); public void setLocation(String location); public String getLocation(); public void rampage(); public String getOriginStory(); } getLocation() is new in this version of Monster
19
Crypt package monsters; public class Crypt { private String location; public Crypt(String location) { this.location = location; } public void setLocation(String location) { public String getLocation() { return location; public String toString(){ return "a mysterious crypt in " + location;
20
Vampire package monsters; public class Vampire implements Monster, Cloneable { private String name; private Crypt crypt; public Vampire(String name, String location) { this.name = name; crypt = new Crypt(location); public void setName(String name) { public String getName() { return name; public void setLocation(String location) { crypt.setLocation(location);// TODO Auto-generated method stub public String getLocation(){ return crypt.getLocation();
21
@Override public String getOriginStory() { return "undead creature which lives by sucking the blood of living humans"; } public void rampage() { StringBuilder sb = new StringBuilder(name + " arises from " + crypt.toString() + " and "); if (crypt.getLocation().equals("Transylvania")) sb.append("sucks people's blood all night, then returns to a coffin to hide from sunlight"); else if (crypt.getLocation().equals("Burbank")) sb.append("takes over the entire television industry"); else { System.out.println("wreaks unknown havoc in fresh Vampire territory"); return; System.out.println(sb); public Object clone() { Vampire newV; try { /* Object clone() returns an Object. It will be a Vampire, but in order to get to anything specific to Vampires, we need to cast it to a Vampire and use a Vampire reference variable */ newV = (Vampire) super.clone(); newV.crypt= new Crypt(crypt.getLocation()); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; return newV;
22
Unit Testing
23
Unit Testing
24
Unit Testing
25
Unit Testing JUnit tests are identified with the annotation @Test:
public void testCryptCreated(){ String location = "Transylvania"; Crypt c = new Crypt(location); assertNotNull(c); } public void testToString(){ assertNotNull(c.toString());
26
JUnit JUnit starts us off with a test that is directed to fail:
27
JUnit
28
Test Failure
29
Unit Testing JUnit Assertions require imports
JUnit Assertions require static imports
30
Test Success
31
Unit Testing Write assertions that will fail if likely errors occur
Write a separate test case (file containing tests) for each class you need to test, even if they implement common interfaces or superclasses. Keep the tests simple. Most tests have only one assertion each. This way, you can identify problems quickly when assertions fail. Systematically exercise the whole interface In this case, "interface" means the public interface of the class being tested. That may be defined partially or completely by a Java interface, an abstract class, or a concrete superclass, or it may be unique to the class. Unit testing is not used for private methods; if these are wrong, any errors should come to light through the public interface Don't reuse objects in multiple tests; instantiate new ones for each test Tests should not have dependencies on each other, which would cause tests to break if other tests are changed
32
Unit Testing package test; import static org.junit.Assert.*; import monsters.Crypt; import org.junit.Test; public class CryptTester public void testCryptCreated(){ String location = "Transylvania"; Crypt c = new Crypt(location); assertNotNull(c); } public void testCryptLocation(){ assertEquals(c.getLocation(), location); public void testSetCryptLocation(){ String firstLocation = "Transylvania"; Crypt c = new Crypt(firstLocation); String secondLocation = "Wisborg"; c.setLocation(secondLocation); assertEquals(c.getLocation(), secondLocation); public void testToString(){ assertNotNull(c.toString());
33
VampireTester Vampire has a Crypt (remember, this is composition).
There is no public method in Vampire that returns the Crypt, so we can’t directly test that it is correctly creating the crypt. This is white box testing, though, and we do know that Vampire.getLocation() gets the location from Crypt.getLocation() @Test public void testLocation() { String name = "Orlok"; String location = "Transylvania"; Vampire v = new Vampire(name, location); assertEquals(v.getLocation(), location); }
34
VampireTester Here are some tests of Vampire.clone() @Test
public void testCloneIsNewVampire(){ String name = "Orlok"; String location = "Transylvania"; Vampire v1 = new Vampire(name, location); Vampire v2 = (Vampire) v1.clone(); //clone() returns an object, but it is a Vampire assertNotSame(v1, v2); } public void testCloneName(){ assertTrue(v1.getName().equals(v2.getName())); public void testCloneLocation(){ assertTrue(v1.getLocation().equals(v2.getLocation())); public void testCloneChangeLocation(){ Vampire v2 = (Vampire) v1.clone(); v2.setLocation("Burbank"); assertFalse(v1.getLocation().equals(v2.getLocation()));
35
VampireTester To refactor code is to change the implementation
When refactoring a class, don't change the public interface unless you can do a major reorganization of the whole application Refactoring should be completely invisible from outside the class, so that other code does not have to change and other programmers don’t have to learn the internal workings of your code Changing the interface inherently means other code must change Let's refactor Crypt.
36
VampireTester Refactor Crypt without introducing any errors:
public String toString(){ return "a very, very mysterious crypt in " + location; } All test results remain the same
37
VampireTester Let's say we refactor Crypt and make a mistake:
public void setLocation(String location) { location = location; } Both CryptTester and VampireTester contain tests that will now fail
38
VampireTester
39
VampireTester Click on *each* line in the JUnit output indicating a failed test
40
Skip A Test When we notice that the errors in Vampire all involve Crypt location, it is a tip off to look closely at Crypt rather than Vampire. The failing test in CryptTester uses Crypt.setLocation() and Crypt.getLocation(), further isolating the problem Find the problem, fix it, and run the tests again.
41
Skip A Test If you find multiple problems and want to deal with them separately, you can skip a test this way: @Ignore @Test public void testSetCryptLocation(){ String firstLocation = "Transylvania"; Crypt c = new Crypt(firstLocation); String secondLocation = "Wisborg"; c.setLocation(secondLocation); assertEquals(c.getLocation(), secondLocation); } You may need to use <ctrl><shift>o to import I strongly recommend you don’t leave failing tests aside. Instead, fix all known problems before coding anything else. If the code you are testing is incorrect, it may cause hard-to-find errors elsewhere.
42
Unit Testing Run all the tests periodically to find errors caused by later code breaking things that worked before or by implementation changes in one part of a system This is the simplest instance of the concept of regression testing. Regression means "going back". Regression testing "goes back" to test code again to see if it still works correctly.
43
Test-Driven Development
Some developers use unit testing as a planning tool by writing the tests before the classes themselves. The class is correct when it passes all the tests (meets the contract) Tests can also serve as a form of documentation; they show how the classes are intended to be used Written documentation is always out of date, but if you change the code in response to new requirements without changing the tests, you'll find out immediately and have to fix it! TDD is popular with agile developers, who emphasize quick development and frequent incremental releases, not formal planning In e-commerce, social media, etc., developers would always lag behind customer demands ("be late to the market") if they wrote detailed documents and didn’t release anything until the "final" product was ready. This doesn’t work for everything; you wouldn't write an antilock braking system or a Mars lander this way
44
Unit Testing JUnit has many more advanced capabilities. As your applications become more complex, periodically look at
45
Java Assertions Note that Java Assertions are different from the various types of JUnit Assertions. Make sure you understand this well enough not to accidentally use Java assertions when you mean to use Junit assertions. Check the imports in your test cases Java assertions that are false throw exceptions only if assertions are enabled in the JVM (since you would not want this in released code), so if you accidentally use them in JUnit, it may appear that all assertions pass, even when something is wrong
46
Java Assertions You don’t need to use Java assertions in this class, only Junit ones, and *don't* use the java assertions in Junit tests. However, if you want to try them out, here is how to set Eclipse to use them.
47
Java Assertions
48
Java Assertions
49
Preview of Generics Recall that a list is a list of objects of some type. We show that using syntax like this: List<Student> students; List is a generic data structure, meaning that we can have lists of many different types of objects. It is parameterized by the data type that the list will hold. Interfaces may be parameterized in the same way. For example, when we declared that Student implemented Comparable, the interface was parameterized by the same class Student. This means that the compareTo() method compares the current Student to another Student.
50
Preview of Generics Lab 7 uses a parameterized interface called MyMath. The Methods of MyMath implement binary operations on classes that implement the interface. Binary operations are operations that involve two instances of a data type Arithmetic addition is a binary operation involving two real numbers (eg, ) In Lab 6, you will write methods that perform operations on two fractions or two sets of integers.
51
Preview of Generics MyMath is parameterized by a data type. This is shown in the interface code using T as a placeholder for the type. Your classes that implement MyMath will be parameterized by the names of the same classes, ie MyFraction implements MyMath<MyFraction> This means that the binary operations required by MyMath and implemented in MyFraction are performed between the current object and an object of type MyFraction (another MyFraction or the same one). For example MyFraction.add(MyFraction o) adds this MyFraction to another MyFraction and returns a reference to the resulting MyFraction.
Similar presentations
© 2025 SlidePlayer.com Inc.
All rights reserved.