mocking, refactoring, etc. - rice...

50

Upload: tranlien

Post on 13-Jun-2018

219 views

Category:

Documents


0 download

TRANSCRIPT

Mocking, refactoring, etc.Dan S. Wallach and Mack Joyner, Rice University

Copyright © 2016 Dan Wallach, All Rights Reserved

Today: a handful of useful tricks

Mocks: a way to test your code before you’ve written it all

Refactoring: how to rearrange your code without killing yourself

Regression testing: keep your old problems from coming back

“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 W. Kernighan

Mock testing

What’s a mock?

A “mock” class acts just like the original class But has none of the actual code in it

Instead, a “mock” just maps inputs to outputs If you call me with this, return that

Why mocks?

Let’s say you need a “troll” for your adventure game But you don’t have one yet

And you need to test your sword (“HIT TROLL WITH SWORD”) Build a “fake” troll that acts in some dumb, deterministic way Plug your “fake” troll into your test harness, verify your sword works

Simple example: CarsWe don’t have a Ferrari yet, but we want to test with one Let’s create a “Car” that we can pass to somebody import static org.mockito.Mockito.*;

public void testFerrari() throws Exception { Car mockFerrari = mock(Car.class); mockFerrari.fireMachineGuns(); verify(mockFerrari).fireMachineGuns(); // ensure that the machine guns were fired}

You can pass this “mock” car anywhere a real Car will go Mockito quietly creates a new subclass that extends Car.

Simple example: CarsWe don’t have a Ferrari yet, but we want to test with one Let’s create a “Car” that we can pass to somebody import static org.mockito.Mockito.*;

public void testFerrari() throws Exception { Car mockFerrari = mock(Car.class); mockFerrari.fireMachineGuns(); verify(mockFerrari).fireMachineGuns(); // ensure that the machine guns were fired}

You can pass this “mock” car anywhere a real Car will go Mockito quietly creates a new subclass that extends Car. Note: only supported in

your “test” classes, not your “main” classes.

Simple example: Cars (2)

Assume each Car has a getValue() method (= monetary value) You can specify the result of any method for a mock! public void testFerrari() throws Exception { Car mockFerrari = mock(Car.class); when(mockFerrari.getValue()).thenReturn(250000.0); assertEquals((Double) 250000.0, (Double) mockFerrari.getValue());}

Pass this mockFerrari anywhere a Car can go!

Verifying your lazy lists are lazy

We want to verify that a lambda is never called until needed public void testLaziness() throws Exception { Supplier<IList<String>> supplier = mock(Supplier.class); when(supplier.get()).thenReturn(LazyList.make("Eve")); IList<String> lazy1 = LazyList.make("Dorothy", supplier); IList<String> lazy = LazyList.of("Alice", "Bob", "Charlie"); IList<String> longerList = lazy.concat(lazy1); verify(supplier, never()).get(); IList<String> ucaseList = longerList.map(String::toUpperCase); assertEquals("ALICE", ucaseList.head()); verify(supplier, never()).get(); assertEquals("BOB", ucaseList.tail().head()); verify(supplier, never()).get(); assertEquals("CHARLIE", ucaseList.tail().tail().head()); verify(supplier, never()).get(); assertEquals("DOROTHY", ucaseList.tail().tail().tail().head()); verify(supplier, never()).get(); assertEquals("EVE", ucaseList.tail().tail().tail().tail().head()); verify(supplier, atLeastOnce()).get(); }

Verifying your lazy lists are lazy

We want to verify that a lambda is never called until needed public void testLaziness() throws Exception { Supplier<IList<String>> supplier = mock(Supplier.class); when(supplier.get()).thenReturn(LazyList.make("Eve")); IList<String> lazy1 = LazyList.make("Dorothy", supplier); IList<String> lazy = LazyList.of("Alice", "Bob", "Charlie"); IList<String> longerList = lazy.concat(lazy1); verify(supplier, never()).get(); IList<String> ucaseList = longerList.map(String::toUpperCase); assertEquals("ALICE", ucaseList.head()); verify(supplier, never()).get(); assertEquals("BOB", ucaseList.tail().head()); verify(supplier, never()).get(); assertEquals("CHARLIE", ucaseList.tail().tail().head()); verify(supplier, never()).get(); assertEquals("DOROTHY", ucaseList.tail().tail().tail().head()); verify(supplier, never()).get(); assertEquals("EVE", ucaseList.tail().tail().tail().tail().head()); verify(supplier, atLeastOnce()).get(); }

Normal LazyLists: Suppliers are lambdas that supply the tail values. Not here!

Verifying your lazy lists are lazy

We want to verify that a lambda is never called until needed public void testLaziness() throws Exception { Supplier<IList<String>> supplier = mock(Supplier.class); when(supplier.get()).thenReturn(LazyList.make("Eve")); IList<String> lazy1 = LazyList.make("Dorothy", supplier); IList<String> lazy = LazyList.of("Alice", "Bob", "Charlie"); IList<String> longerList = lazy.concat(lazy1); verify(supplier, never()).get(); IList<String> ucaseList = longerList.map(String::toUpperCase); assertEquals("ALICE", ucaseList.head()); verify(supplier, never()).get(); assertEquals("BOB", ucaseList.tail().head()); verify(supplier, never()).get(); assertEquals("CHARLIE", ucaseList.tail().tail().head()); verify(supplier, never()).get(); assertEquals("DOROTHY", ucaseList.tail().tail().tail().head()); verify(supplier, never()).get(); assertEquals("EVE", ucaseList.tail().tail().tail().tail().head()); verify(supplier, atLeastOnce()).get(); }

When the (mock) lambda is invoked, return the next step in the LazyList.

Verifying your lazy lists are lazy

We want to verify that a lambda is never called until needed public void testLaziness() throws Exception { Supplier<IList<String>> supplier = mock(Supplier.class); when(supplier.get()).thenReturn(LazyList.make("Eve")); IList<String> lazy1 = LazyList.make("Dorothy", supplier); IList<String> lazy = LazyList.of("Alice", "Bob", "Charlie"); IList<String> longerList = lazy.concat(lazy1); verify(supplier, never()).get(); IList<String> ucaseList = longerList.map(String::toUpperCase); assertEquals("ALICE", ucaseList.head()); verify(supplier, never()).get(); assertEquals("BOB", ucaseList.tail().head()); verify(supplier, never()).get(); assertEquals("CHARLIE", ucaseList.tail().tail().head()); verify(supplier, never()).get(); assertEquals("DOROTHY", ucaseList.tail().tail().tail().head()); verify(supplier, never()).get(); assertEquals("EVE", ucaseList.tail().tail().tail().tail().head()); verify(supplier, atLeastOnce()).get(); }

At each step of reading the LazyList, we can verify that the Supplier was never called.

Couldn’t I have done that without Mockito?Yes. You could have made a Supplier that mutated some state: class Tripwire { boolean tripped = false; }

public void testLaziness2() throws Exception { Tripwire tripwire = new Tripwire(); Supplier<IList<String>> supplier = () -> { tripwire.tripped = true; return LazyList.make("Eve"); }; IList<String> lazy1 = LazyList.make("Dorothy", supplier); IList<String> lazy = LazyList.of("Alice", "Bob", "Charlie"); IList<String> longerList = lazy.concat(lazy1); assertTrue(!tripwire.tripped); IList<String> ucaseList = longerList.map(String::toUpperCase); assertEquals("ALICE", ucaseList.head()); assertTrue(!tripwire.tripped);

Mockito can also “spy” on a real objectThis works for class instances, but not lambdas. Function<Integer,Integer> incrementer = x -> x + 1; public void makeOnlyOnce() throws Exception { Function<Integer, Integer> spyIncrementer = spy(incrementer); Function<Integer,Integer> memoizedIncrementer = MemoizedFunction.make(spyIncrementer);

Mockito can also “spy” on a real objectThis works for class instances, but not lambdas. Function<Integer,Integer> incrementer = x -> x + 1; public void makeOnlyOnce() throws Exception { Function<Integer, Integer> spyIncrementer = spy(incrementer); Function<Integer,Integer> memoizedIncrementer = MemoizedFunction.make(spyIncrementer);

Cannot mock/spy class edu.rice.dynamic.MemoizedFunctionTest$$Lambda$1/1971489295 Mockito cannot mock/spy following: - final classes - anonymous classes - primitive types

Mockito can also “spy” on a real objectHere’s a test for our edu.rice.dynamic.MemoizedFunction This “delegating” trick lets us spy on a real lambda. Function<Integer,Integer> incrementer = x -> x + 1; public void makeOnlyOnce() throws Exception { @SuppressWarnings("unchecked") Function<Integer, Integer> spyIncrementer = mock(Function.class, AdditionalAnswers.delegatesTo(incrementer)); Function<Integer,Integer> memoizedIncrementer = MemoizedFunction.make(spyIncrementer); verify(spyIncrementer, never()).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, never()).apply(4); assertEquals((Integer) 2, memoizedIncrementer.apply(1)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); verify(spyIncrementer, atMost(1)).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, atMost(1)).apply(4);}

Mockito 1.x doesn’t understand lambdas. Mockito 2.x is a work in progress.

Hopefully this will eventually be cleaner.

Mockito can also “spy” on a real objectHere’s a test for our edu.rice.dynamic.MemoizedFunction This “delegating” trick lets us spy on a real lambda. Function<Integer,Integer> incrementer = x -> x + 1; public void makeOnlyOnce() throws Exception { @SuppressWarnings("unchecked") Function<Integer, Integer> spyIncrementer = mock(Function.class, AdditionalAnswers.delegatesTo(incrementer)); Function<Integer,Integer> memoizedIncrementer = MemoizedFunction.make(spyIncrementer); verify(spyIncrementer, never()).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, never()).apply(4); assertEquals((Integer) 2, memoizedIncrementer.apply(1)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); verify(spyIncrementer, atMost(1)).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, atMost(1)).apply(4);}

We’re using the “real” MemoziedFunction with a “mocked” lambda.

Mockito can also “spy” on a real objectHere’s a test for our edu.rice.dynamic.MemoizedFunction This “delegating” trick lets us spy on a real lambda. Function<Integer,Integer> incrementer = x -> x + 1; public void makeOnlyOnce() throws Exception { @SuppressWarnings("unchecked") Function<Integer, Integer> spyIncrementer = mock(Function.class, AdditionalAnswers.delegatesTo(incrementer)); Function<Integer,Integer> memoizedIncrementer = MemoizedFunction.make(spyIncrementer); verify(spyIncrementer, never()).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, never()).apply(4); assertEquals((Integer) 2, memoizedIncrementer.apply(1)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); verify(spyIncrementer, atMost(1)).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, atMost(1)).apply(4);}

The property we care about: verifying that the lambda was called at most once.

Mockito can also “spy” on a real objectHere’s a test for our edu.rice.dynamic.MemoizedFunction This “delegating” trick lets us spy on a real lambda. Function<Integer,Integer> incrementer = x -> x + 1; public void makeOnlyOnce() throws Exception { @SuppressWarnings("unchecked") Function<Integer, Integer> spyIncrementer = mock(Function.class, AdditionalAnswers.delegatesTo(incrementer)); Function<Integer,Integer> memoizedIncrementer = MemoizedFunction.make(spyIncrementer); verify(spyIncrementer, never()).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, never()).apply(4); assertEquals((Integer) 2, memoizedIncrementer.apply(1)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); assertEquals((Integer) 5, memoizedIncrementer.apply(4)); verify(spyIncrementer, atMost(1)).apply(1); verify(spyIncrementer, never()).apply(2); verify(spyIncrementer, never()).apply(3); verify(spyIncrementer, atMost(1)).apply(4);}

When to use Mockito?Read the documentation: http://mockito.org https://dzone.com/refcardz/mockito http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html

Ask yourself: where might I drop in a “mock” object instead of a real one? Treap: mock left/right child treats with fixed priorities… test rebalancing Adventure: mock creatures / artifacts in the game User interaction, files, networks: mock versions return fixed values

Mockito is only for testing! Performance would be unacceptable in “production” code

Refactoring

You’ve probably noticed:

IntelliJ has lots of refactoring support Easy: rename a variable or a class More interesting: move a static method to another class Really fun: change the order of arguments for a function

IntelliJ will make the changes everywhere We used this to rearrange/rename huge chunks of the edu.rice classes between last and this year!

Radically less error-prone than doing it manually

Refactoring is many things

Stuff in the IntelliJ menus: Conceptually simple ideas (rename a method, rename a variable, etc.) Avoid the error-prone process of doing it manually

Other refactoring concepts require you to do changes manually HIT TROLL WITH SWORD Where’s the logic to compute damage to the troll?

• In the player, the sword, or the troll? If you change your mind, you’ll be rearranging things manually.

Refactoring versus last year’s Comp215 library

New class structure (the “Comp215 standard structure”) with outer interfaces, inner classes Originally, “class Empty extends List”

• Java inheritance in week 1= confusion! Several days of code rearrangement to get it all working IntelliJ tools helped for some things, not others

(And we also refactored slides from the lectures…)

Example 1: Handling empty lists

Last year: IList<String> emptyList = List.Empty.create();

This year: IList<String> emptyList = List.makeEmpty();

IntelliJ made it easy to “move” the static method then rename it But unit tests were essential to making sure nothing broke! Doing this manually would have been very painful.

Example 2: Option-stacks in RPNLast year: static Optional<IList<Double>> add(Optional<IList<Double>> ostack) { return ostack.flatMap(stack -> { if (stack.empty() || stack.tail().empty()) return Optional.empty(); // need two values double result = stack.head() + stack.tail().head(); return Optional.of(stack.tail().tail().add(result)); });}

This year: static OStack add(OStack ostack) { return ostack.flatmap( stack -> stack.match( empty -> OStack.none(), (head, empty) -> OStack.none(), (head, second, tail) -> OStack.some(tail.add(head + second))));}

Four major changes: • Optional ➜ Option • Optional<IList<Double>> ➜ OStack • UnaryOperator<Optional<IList<Double>>> ➜ CalcOp • if, tail() and head() ➜ match

A manually intensive process, but much cleaner code!

Refactoring / code cleanup

Code cleanup is essential to code maintenance Alternatively: you accumulate “technical debt” and have more work later https://en.wikipedia.org/wiki/Technical_debt

Example: Netscape Navigator (circa 1996-1997) Netscape 1.x was single-threaded, complex rules about blocking Netscape 2.x was multi-threaded in new code, single-threaded core

• Constantly getting stuck, need to restart Major rewrite wasn’t considered feasible (“browser wars”)

Engineering advice: start early!

If your approach runs into a brick wall (and it eventually will): Backtrack in your head: how should the code look? Can you get there? How much pain to rearrange your code? Easier to “start over” and paste in old code as necessary?

• Sometimes yes, sometimes no.

Plan in advance, make tiny experiments Before rearranging everything, try something new and small.

Regression testing

The worst feeling in the world“Hey, didn’t I fix that already?”

The worst feeling in the world“Hey, didn’t I fix that already?”

The worst feeling in the world“Hey, didn’t I fix that already?”

The worst feeling in the world“Hey, didn’t I fix that already?”

This is an ancient bug that was actually attempted to be fixed once (badly) by me eleven years ago in commit 4ceb5db9757a (“Fix get_user_pages() race for write access") but that was then undone due to problems on s390 by commit f33ea7f404e5 ("fix get_user_pages bug”).

- Linus Torvalds (20 October 2016)

How do you prevent regressions?

Write lots of unit tests! Every time you fix a bug, write a unit test to verify the fix.

Example: files with spaces in their namesWant a list of filenames in a given resource directory? It might be files in the filesystem It might be entries in a Jar file (Jar = Zip)

The Comp215 code tries to work correctly in both cases

But in last night’s lab, a student told Dr. Wallach: “This unit test has always failed for me, but it doesn’t seem to be a problem.”

Quick hypothesis: filenames with a space in the name are a problem Logged error messages include “%20” where the spaces should go (%20 is how a URL will escape a whitespace in a filename)

Logging FTW!

Example: files with spaces in their namesWant a list of filenames in a given resource directory? It might be files in the filesystem It might be entries in a Jar file (Jar = Zip)

The Comp215 code tries to work correctly in both cases

But in last night’s lab, a student told Dr. Wallach: “This unit test has always failed for me, but it doesn’t seem to be a problem.”

Quick hypothesis: filenames with a space in the name are a problem Logged error messages include “%20” where the spaces should go (%20 is how a URL will escape a whitespace in a filename)

Quick fix: Run a URL unescaperAdded one line of code, changed four other lines

Existing unit tests passed But we’re not done! Dr. Wallach doesn’t have spaces in his file paths.

New unit tests added Whitespace in directory and file names. New tests now pass.

But is the problem really fixed? Technical debt: increasingly ugly code in edu.rice.io.Files Refactoring to-do: dig deeper into Javadoc for file directory reading

• And as well for how Jar file reading works Doing this right: probably a solid day of work

Why write new unit tests?

Try to make the original problem reproducible A good unit test will recreate the problem on every computer

• Not just the one student with whitespace in a directory name

Fix the problem Verify it works, and that all the old tests pass

If any of the unit tests don’t pass… The “fix” may have caused more problems

Regression is a thing that happens

Sometimes explicitly: you roll back to a last-known “good” state And you may forget that you fixed some bugs along the way

Sometimes implicitly: you resolved some problem incorrectly Maybe you’re borrowing incorrect solutions from StackOverflow Comp215 fun: we have one “branch” per week

• Need to apply bug fixes to every relevant week • What if we missed a week?

Detailed unit tests will detect regressions!

Fun in your future: group projects

Comp215: you’re working on your own The course staff is your “partner”

Many other classes (and the real world): you work in teams Changes in code that you didn’t write can break your code!

Solution? Write more unit tests! Write a unit test that exercises a library how you need it. If the library changes, your test fails, the library author will see that.

Track your bugs and to-do list

Keep it simple

Make a file, “TODO.txt” Write your notes to yourself

Slightly fancier: IntelliJ looks for “TODO” in any comment Use IntelliJ to find all your TODO items inside your own code!

How do the “pros” do it?Every “real” software management system has bug tracking Create bugs, assign them to devs, track their progress, etc.

How do the “pros” do it?Every “real” software management system has bug tracking Create bugs, assign them to devs, track their progress, etc.

Notes to myself about what I want to do in the edu.rice codebase. Not necessarily meaningful to anybody else.

A “good” bug report includes sample code that triggers the problem.

Here, a simplified version of the match() methods on IList, which caused older IntelliJ versions to fail at type inference.

“If debugging is the process of removing software bugs, then

programming must be the process of putting them in.”

- Edsger Dijkstra

Practice with having a “process”

Projects 1-9: you can keep everything in your head

Projects 10+: complicated enough that you might forget things Start using a process now, even a cheesy one Develop good habits now