56
HELP MY Tests ARE KILLING ME!! brian swan @bgswan follow me on twitter

Help, my tests are killing me!!

Embed Size (px)

DESCRIPTION

Since the early days of eXtreme Programming, tests have been touted as "executable documentation". In practice, however, many tests fail to live up to that ideal and worse become a time sink when trying to decipher test failures. Using examples and focusing specifically on unit or developer tests, this talk will examine the characteristics of readability in tests and offer advice on making your tests more readable.

Citation preview

Page 1: Help, my tests are killing me!!

HELPMY Tests ARE KILLING ME!!

brian swan @bgswanfollow meon twitter

Page 2: Help, my tests are killing me!!

Contains code some people may find offensive

Page 3: Help, my tests are killing me!!

Testsuite: org.springframework.util.StopWatchTestsTests run: 1, Failures: 1, Errors: 0, Time elapsed: 0.177 sec

Testcase: testValidUsage took 0.171 sec FAILEDUnexpected timing 167junit.framework.AssertionFailedError: Unexpected timing 167 at org.springframework.util.StopWatchTests.testValidUsage(StopWatchTests.java:48)

Page 4: Help, my tests are killing me!!

public void testValidUsage() throws Exception { StopWatch sw = new StopWatch(); long int1 = 166L; long int2 = 45L; String name1 = "Task 1"; String name2 = "Task 2";

assertFalse(sw.isRunning()); sw.start(name1); Thread.sleep(int1); assertTrue(sw.isRunning()); sw.stop();

long fudgeFactor = 0L; assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + fudgeFactor); sw.start(name2); Thread.sleep(int2); sw.stop(); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1 + int2); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + int2 + fudgeFactor);

assertTrue(sw.getTaskCount() == 2); String pp = sw.prettyPrint(); assertTrue(pp.indexOf(name1) != -1); assertTrue(pp.indexOf(name2) != -1);

StopWatch.TaskInfo[] tasks = sw.getTaskInfo(); assertTrue(tasks.length == 2); assertTrue(tasks[0].getTaskName().equals(name1)); assertTrue(tasks[1].getTaskName().equals(name2)); sw.toString(); }

Page 5: Help, my tests are killing me!!

AARGGHH

Page 6: Help, my tests are killing me!!

public void testValidUsage() throws Exception { StopWatch sw = new StopWatch(); long int1 = 166L; long int2 = 45L; String name1 = "Task 1"; String name2 = "Task 2";

assertFalse(sw.isRunning()); sw.start(name1); Thread.sleep(int1); assertTrue(sw.isRunning()); sw.stop();

// TODO are timings off in JUnit? Why do these assertions sometimes fail // under both Ant and Eclipse?

//long fudgeFactor = 5L; //assertTrue("Unexpected timing " + sw.getTotalTime(), sw.getTotalTime() >= int1); //assertTrue("Unexpected timing " + sw.getTotalTime(), sw.getTotalTime() <= int1 + fudgeFactor); sw.start(name2); Thread.sleep(int2); sw.stop(); //assertTrue("Unexpected timing " + sw.getTotalTime(), sw.getTotalTime() >= int1 + int2); //assertTrue("Unexpected timing " + sw.getTotalTime(), sw.getTotalTime() <= int1 + int2 + fudgeFactor);

assertTrue(sw.getTaskCount() == 2); String pp = sw.prettyPrint(); assertTrue(pp.indexOf(name1) != -1); assertTrue(pp.indexOf(name2) != -1);

StopWatch.TaskInfo[] tasks = sw.getTaskInfo(); assertTrue(tasks.length == 2); assertTrue(tasks[0].getTaskName().equals(name1)); assertTrue(tasks[1].getTaskName().equals(name2)); sw.toString(); }

https://github.com/SpringSource/spring-framework/blob/master/spring-core/src/test/java/org/springframework/util/StopWatchTests.java

Page 7: Help, my tests are killing me!!

The UnREADABLE TEST

produced by Long Test Methods featuring Undecipherable Failures

introducing Developer Confusion and Wasted Developer Time

Page 9: Help, my tests are killing me!!

MORE READABILITY

Produced By SMALL WORDS and SHORT SENTENCES

Page 12: Help, my tests are killing me!!

THELINE LENGTH IDENTIFIER MASSACRE

Page 13: Help, my tests are killing me!!

THE INCREDIBLE

Page 14: Help, my tests are killing me!!

it "adds items to top" do stack = Stack.new # Setup stack.add "thing" # Exercise assert_equal "thing", stack.top # Verify stack = nil # Teardown end

Page 15: Help, my tests are killing me!!

it "adds items to top" do stack = Stack.new # Setup stack.add "thing" # Exercise assert_equal "thing", stack.top # Verify end

Page 16: Help, my tests are killing me!!

it "adds items to top" do stack = Stack.new # Arrange stack.add "thing" # Act assert_equal "thing", stack.top # Assert end

Page 17: Help, my tests are killing me!!

it "adds items to top" do stack = Stack.new # Given stack.add "thing" # When assert_equal "thing", stack.top # Then end

Page 18: Help, my tests are killing me!!

before do @stack = Stack.new # Given end it "adds items to top" do @stack.add "thing" # When assert_equal "thing", @stack.top # Then end

Page 19: Help, my tests are killing me!!

before do @stack = Stack.new # Given end it "is initially empty" do assert_empty @stack # Then end

Page 20: Help, my tests are killing me!!
Page 21: Help, my tests are killing me!!

it "returns the total timesheet hours worked of the timesheet" do timesheet = Timesheet.new task_items = [stub(start_time: 1359291600, end_time: 1359295200), stub(start_time: 1359295200, end_time: 1359298900)] timesheet.stub(:task_items) { task_items } timesheet.calculate_total_hours.should == 2.0 end

http://www.superpumpup.com/sub-ms-test

Page 22: Help, my tests are killing me!!

it "returns the total timesheet hours worked of the timesheet" do timesheet = Timesheet.new task_items = [stub(start_time: 1359291600, end_time: 1359295200), stub(start_time: 1359295200, end_time: 1359298900)] timesheet.stub(:task_items) { task_items } timesheet.calculate_total_hours.should == 2.0 end

Page 23: Help, my tests are killing me!!

Extract Given

You have a long, confusing or poorly named test setup.

Turn the fragment into a method or member variable whose name describes the “given”.

Page 26: Help, my tests are killing me!!

def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new task_items = [stub(start_time: 1359291600, end_time: 1359295200), stub(start_time: 1359295200, end_time: 1359298900)] timesheet.stub(:task_items) { task_items } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 27: Help, my tests are killing me!!

def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new task_items = [stub(start_time: 1359291600, end_time: 1359295200), stub(start_time: 1359295200, end_time: 1359298900)] timesheet.stub(:task_items) { task_items } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 28: Help, my tests are killing me!!

Extract Collaborator

An object your test depends on is confusing or poorly named.

Extract the collaborating object into a method or member variable whose name describes its purpose.

Page 29: Help, my tests are killing me!!

def one_hour_task stub(start_time: 1359291600, end_time: 1359295200) end

def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new task_items = [one_hour_task, one_hour_task] timesheet.stub(:task_items) { task_items } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 30: Help, my tests are killing me!!

def one_hour_task stub(start_time: 1359291600, end_time: 1359295200) end

def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new task_items = [one_hour_task, one_hour_task] timesheet.stub(:task_items) { task_items } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 31: Help, my tests are killing me!!

Inline TempYou have a temp that is assigned to once with a simple expression, and the temp is getting in the way of other refactorings.

Replace all references to that temp with the expression.

http://www.refactoring.com/catalog/inlineTemp.html

Page 32: Help, my tests are killing me!!

def one_hour_task stub(start_time: 1359291600, end_time: 1359295200) end def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new timesheet.stub(:task_items) { [one_hour_task, one_hour_task] } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 33: Help, my tests are killing me!!

def one_hour_task stub(start_time: 1359291600, end_time: 1359295200) end def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new timesheet.stub(:task_items) { [one_hour_task, one_hour_task] } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 34: Help, my tests are killing me!!

Use meaningful data

Your test uses arbitrary literal values as test data.

Replace arbitrary values with values that have meaning in the domain of the test.

Page 35: Help, my tests are killing me!!

ONE_HOUR_IN_SECONDS = (60*60*1)

def one_hour_task stub(start_time: 0, end_time: ONE_HOUR_IN_SECONDS) end

def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new timesheet.stub(:task_items) { [one_hour_task, one_hour_task] } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 36: Help, my tests are killing me!!

ONE_HOUR_IN_SECONDS = (60*60*1)

def one_hour_task stub(start_time: 0, end_time: ONE_HOUR_IN_SECONDS) end

def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new timesheet.stub(:task_items) { [one_hour_task, one_hour_task] } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 37: Help, my tests are killing me!!

Extract Test

Your test method is checking multiple behaviours, or checking behaviour of a collaborating object.

Extract a new test method for each behaviour.

Page 38: Help, my tests are killing me!!

describe Task do

it "knows duration in hours" do task = Task.new(start_time: 0, end_time: 3600) task.duration.should == 1.0 end end

Page 39: Help, my tests are killing me!!

def one_hour_task stub(duration: 1) end

def timesheet_with_two_one_hour_tasks timesheet = Timesheet.new timesheet.stub(:task_items) { [one_hour_task, one_hour_task] } timesheet end

it "returns the total timesheet hours worked of the timesheet" do timesheet_with_two_one_hour_tasks.calculate_total_hours.should == 2.0 end

Page 40: Help, my tests are killing me!!

it "returns the total timesheet hours worked of the timesheet" do one_hour_task = stub(duration: 1) timesheet = Timesheet.new timesheet.stub(:task_items) { [one_hour_task, one_hour_task] }

timesheet.calculate_total_hours.should == 2.0 end

Page 41: Help, my tests are killing me!!

it "returns the total timesheet hours worked of the timesheet" do timesheet = Timesheet.new task_items = [stub(start_time: 1359291600, end_time: 1359295200), stub(start_time: 1359295200, end_time: 1359298900)] timesheet.stub(:task_items) { task_items } timesheet.calculate_total_hours.should == 2.0 end

http://www.superpumpup.com/sub-ms-test

Page 42: Help, my tests are killing me!!

it "returns the total timesheet hours worked of the timesheet" do one_hour_task = stub(duration: 1) timesheet = Timesheet.new timesheet.stub(:task_items) { [one_hour_task, one_hour_task] }

timesheet.calculate_total_hours.should == 2.0 end

Page 43: Help, my tests are killing me!!

IIThe UnREADABLE TEST

Page 44: Help, my tests are killing me!!

public void testValidUsage() throws Exception { StopWatch sw = new StopWatch(); long int1 = 166L; long int2 = 45L; String name1 = "Task 1"; String name2 = "Task 2";

assertFalse(sw.isRunning()); sw.start(name1); Thread.sleep(int1); assertTrue(sw.isRunning()); sw.stop();

long fudgeFactor = 0L; assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + fudgeFactor); sw.start(name2); Thread.sleep(int2); sw.stop(); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1 + int2); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + int2 + fudgeFactor);

assertTrue(sw.getTaskCount() == 2); String pp = sw.prettyPrint(); assertTrue(pp.indexOf(name1) != -1); assertTrue(pp.indexOf(name2) != -1);

StopWatch.TaskInfo[] tasks = sw.getTaskInfo(); assertTrue(tasks.length == 2); assertTrue(tasks[0].getTaskName().equals(name1)); assertTrue(tasks[1].getTaskName().equals(name2)); sw.toString(); }

Page 45: Help, my tests are killing me!!

X...the Unknowntest case

Page 47: Help, my tests are killing me!!

Rename Method

The name of a method does not reveal its purpose.

Change the name of the method.

http://www.refactoring.com/catalog/renameMethod.html

Page 48: Help, my tests are killing me!!

public void testIsRunningWhenStartedAndTimeMultipleTasksAndTaskCountAndPrettyPrintAndTaskInfo() throws Exception { StopWatch sw = new StopWatch(); long int1 = 166L; long int2 = 45L; String name1 = "Task 1"; String name2 = "Task 2";

assertFalse(sw.isRunning()); sw.start(name1); Thread.sleep(int1); assertTrue(sw.isRunning()); sw.stop();

long fudgeFactor = 5L; assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + fudgeFactor); sw.start(name2); Thread.sleep(int2); sw.stop(); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1 + int2); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + int2 + fudgeFactor);

assertTrue(sw.getTaskCount() == 2); String pp = sw.prettyPrint(); assertTrue(pp.indexOf(name1) != -1); assertTrue(pp.indexOf(name2) != -1);

StopWatch.TaskInfo[] tasks = sw.getTaskInfo(); assertTrue(tasks.length == 2); assertTrue(tasks[0].getTaskName().equals(name1)); assertTrue(tasks[1].getTaskName().equals(name2)); sw.toString(); }

Page 49: Help, my tests are killing me!!

public void testTimeMultipleTasks() throws Exception { StopWatch sw = new StopWatch(); long int1 = 166L; long int2 = 45L; String name1 = "Task 1"; String name2 = "Task 2";

sw.start(name1); Thread.sleep(int1); sw.stop();

long fudgeFactor = 5L; assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + fudgeFactor); sw.start(name2); Thread.sleep(int2); sw.stop(); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1 + int2); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + int2 + fudgeFactor); }

Page 50: Help, my tests are killing me!!

public void testTimeMultipleTasks() throws Exception { StopWatch sw = new StopWatch(); StopWatch.TaskInfo task1 = new StopWatch.TaskInfo("Task 1", 166L); StopWatch.TaskInfo task2 = new StopWatch.TaskInfo("Task 2", 45L);

sw.start(task1.getTaskName()); Thread.sleep(task1.getTimeMillis()); sw.stop();

long fudgeFactor = 10L; assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= task1.getTimeMillis()); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <=

task1.getTimeMillis() + fudgeFactor); sw.start(task2.getTaskName()); Thread.sleep(task2.getTimeMillis()); sw.stop(); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= task1.getTimeMillis() + task2.getTimeMillis()); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= task1.getTimeMillis() + task2.getTimeMillis() + fudgeFactor); }

Page 51: Help, my tests are killing me!!

public void testTimeMultipleTasks() throws Exception { StopWatch sw = new StopWatch(); StopWatch.TaskInfo task1 = new StopWatch.TaskInfo("Task 1", 166L); StopWatch.TaskInfo task2 = new StopWatch.TaskInfo("Task 2", 45L);

timeTask(sw, task1);

long fudgeFactor = 10L; assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= task1.getTimeMillis()); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= task1.getTimeMillis() + fudgeFactor);

timeTask(sw, task2);

assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= task1.getTimeMillis() + task2.getTimeMillis()); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= task1.getTimeMillis() + task2.getTimeMillis() + fudgeFactor); }

private void timeTask(StopWatch sw, StopWatch.TaskInfo task) throws Exception { sw.start(task.getTaskName()); Thread.sleep(task.getTimeMillis()); sw.stop(); }

Page 52: Help, my tests are killing me!!

Replace Assertion

Your test method uses a basic assertion to check for a specific condition.

Use a more specific assertion provided by your test framework, or create a custom assert method.

Page 53: Help, my tests are killing me!!

public void testTimeMultipleTasks() throws Exception { StopWatch sw = new StopWatch(); StopWatch.TaskInfo task1 = new StopWatch.TaskInfo("Task 1", 166L); StopWatch.TaskInfo task2 = new StopWatch.TaskInfo("Task 2", 45L);

timeTask(sw, task1); assertElapsedTime(sw, task1.getTimeMillis());

timeTask(sw, task2); assertElapsedTime(sw, task1.getTimeMillis() + task2.getTimeMillis()); }

private void assertElapsedTime(StopWatch sw, int expectedDuration) { double tolerance = 10.0; assertThat((double)sw.getTotalTimeMillis(), is(closeTo((double)expectedDuration, tolerance))); }

private void timeTask(StopWatch sw, StopWatch.TaskInfo task) throws Exception { sw.start(task.getTaskName()); Thread.sleep(task.getTimeMillis()); sw.stop(); }

Page 54: Help, my tests are killing me!!

public void testValidUsage() throws Exception { StopWatch sw = new StopWatch(); long int1 = 166L; long int2 = 45L; String name1 = "Task 1"; String name2 = "Task 2";

assertFalse(sw.isRunning()); sw.start(name1); Thread.sleep(int1); assertTrue(sw.isRunning()); sw.stop();

long fudgeFactor = 0L; assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + fudgeFactor); sw.start(name2); Thread.sleep(int2); sw.stop(); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() >= int1 + int2); assertTrue("Unexpected timing " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() <= int1 + int2 + fudgeFactor);

assertTrue(sw.getTaskCount() == 2); String pp = sw.prettyPrint(); assertTrue(pp.indexOf(name1) != -1); assertTrue(pp.indexOf(name2) != -1);

StopWatch.TaskInfo[] tasks = sw.getTaskInfo(); assertTrue(tasks.length == 2); assertTrue(tasks[0].getTaskName().equals(name1)); assertTrue(tasks[1].getTaskName().equals(name2)); sw.toString(); }

Page 55: Help, my tests are killing me!!

public void testTimeMultipleTasks() throws Exception { StopWatch sw = new StopWatch(); StopWatch.TaskInfo task1 = new StopWatch.TaskInfo("Task 1", 166L); StopWatch.TaskInfo task2 = new StopWatch.TaskInfo("Task 2", 45L);

timeTask(sw, task1); assertElapsedTime(sw, task1.getTimeMillis());

timeTask(sw, task2); assertElapsedTime(sw, task1.getTimeMillis() + task2.getTimeMillis()); }

Page 56: Help, my tests are killing me!!

Readability Short Test Methods

Shorter Line Length

Fewer Identifiers

Clear Test Method Names

Refactorings Extract Given

Extract Collaborator

Use Meaningful Data

Extract Test

Replace Assertion