unit testing legacy code

31
Unit testing legacy code Lars Thorup ZeaLake Software Consulting May, 2014

Upload: lars-thorup

Post on 08-May-2015

410 views

Category:

Software


4 download

DESCRIPTION

Unit testing and test-driven development are practices that makes it easy and efficient to create well-structured and well-working code. However, many software projects didn't create unit tests from the beginning. In this presentation I will show a test automation strategy that works well for legacy code, and how to implement such a strategy on a project. The strategy focuses on characterization tests and refactoring, and the slides contain a detailed example of how to carry through a major refactoring in many tiny steps

TRANSCRIPT

Page 1: Unit testing legacy code

Unit testinglegacy codeLars ThorupZeaLake Software Consulting

May, 2014

Page 2: Unit testing legacy code

Who is Lars Thorup?

● Software developer/architect● JavaScript, C#● Test Driven Development● Continuous Integration

● Coach: Teaching TDD and continuous integration

● Founder of ZeaLake

● @larsthorup

Page 3: Unit testing legacy code

The problems with legacy code● No tests

● The code probably works...

● Hard to refactor● Will the code still work?

● Hard to extend● Need to change the code...

● The code owns us :(● Did our investment turn sour?

Page 4: Unit testing legacy code

How do tests bring us back in control?

● A refactoring improves the design without changing behavior

● Tests ensure that behavior is not accidentally changed

● Without tests, refactoring is scary● and with no refactoring, the design decays over time

● With tests, we have the courage to refactor● so we continually keep our design healthy

Page 5: Unit testing legacy code

What comes first: the test or the refactoring?

Page 6: Unit testing legacy code

How do we get to sustainable legacy code?● Make it easy to add characterization tests

● Have good unit test coverage for important areas● Don't worry about code you don't need to change

● Test-drive all new code

● Now we own the code :)

Page 7: Unit testing legacy code

Making legacy code sustainable● Select an important area

● Driven by change requests

● Add characterization tests

● Make code testable

● Refactor the code

● Add unit tests

● Remove characterization tests

● Small steps

Page 8: Unit testing legacy code

Characterization tests● Characterize current

behavior

● Integration tests● Either high level unit tests● Or end-to-end tests

● Don't change existing code● Faulty behavior = current

behavior: don't change it!● Make a note to fix later

● Test at a level that makes it easy

● The characterization tests are throw-aways

● Demo: ● Web service test: VoteMedia● End-to-end browser test:

entrylist.demo.test.js

Page 9: Unit testing legacy code

Make code testable● Avoid large methods

● They require a ton of setup● They require lots of scenarios to cover all variations

● Avoid outer scope dependencies● They require you to test at a higher level

● Avoid external dependencies● ... a ton of setup● They slow you down

Page 10: Unit testing legacy code

Refactor the code● Add interface

● Inject a mock instead of the real thing

● Easier setup● Infinitely faster

Notifier

EmailSvc

IEmailSvc

EmailSvcStub

NotifierTest

● Extract method● Split up large methods● To simplify unit testing single

behaviors● Demo:

VoteWithVideo_Vimas

● Add parameter● Pass in outer-scope

dependencies● The tests can pass in their

own dummy values● Demo:

Entry.renderResponse

Page 11: Unit testing legacy code

Add unit tests● Now that the code is testable...

● Write unit tests for you small methods

● Pass in dummy values for parameters

● Mock dependencies

● Rinse and repeat...

Page 12: Unit testing legacy code

Remove the characterization tests● When unit test code coverage is good enough

● To speed up feedback

● To avoid test duplication

Page 13: Unit testing legacy code

Small steps - elephant carpaccio● Any big refactoring...

● ...can be done in small steps

● Demo: Security system (see slide 19 through 31)

Page 14: Unit testing legacy code

Test-drive all new code● Easy, now that unit testing tools are in place

Failingtest

Succeedingtest

Gooddesign Refactor

Test

IntentionThink, talk

Code

Page 15: Unit testing legacy code

Making legacy code sustainable● Select an important area

● Driven by change requests

● Add characterization tests

● Make code testable

● Refactor the code

● Add unit tests

● Remove characterization tests

● Small steps

Page 16: Unit testing legacy code

It's not hard - now go do it!● This is hard

● SQL query efficiency● Cache invalidation● Scalability● Pixel perfect rendering● Cross-browser compatibility● Indexing strategies● Security● Real time media streaming● 60fps gaming with HTML5

● ... and robust Selenium tests!

● This is not hard● Refactoring● Unit testing● Dependency injection● Automated build and test● Continuous Integration

● Fast feedback will make you more productive

● ... and more happy

Page 17: Unit testing legacy code

A big refactoring is needed...

Page 18: Unit testing legacy code

Avoid feature branches● For features as well as large refactorings

● Delayed integration● Increases risk● Increases cost

Page 19: Unit testing legacy code

Use feature toggles● Any big refactoring...

● ...can be done in small steps

● Allows us to keep development on trunk/master

● Drastically lowering the risk

● Commit after every step● At most a couple of hours

Page 20: Unit testing legacy code

Security example● Change the code from the

old security system

● To our new extended security model

interface IPrivilege

{

bool HasRole(Role);

}

class Permission

{

bool IsAdmin();

}

Page 21: Unit testing legacy code

Step 0: existing implementation● Code instantiates

Legacy.Permission

● and calls methods like permission.IsAdmin()

● ...all over the place

● We want to replace this with a new security system

void SomeController()

{

var p = new Permission();

if (p.IsAdmin())

{

...

}

}

Page 22: Unit testing legacy code

Step 1: New security implementation● Implements an interface

● This can be committed gradually

interface IPrivilege

{

bool HasRole(Role);

}

class Privilege : IPrivilege

{

bool HasRole(Role r)

{

...

}

}

Page 23: Unit testing legacy code

Step 2: Wrap the old implementation● Create

Security.LegacyPermission

● Implement new interface

● Wrap existing implementation

● Expose existing implementation

class LegacyPermission : IPrivilege

{

LegacyPermission(Permission p)

{

this.p = p;

}

bool HasRole(Role r)

{

if (r == Role.Admin)

return p.IsAdmin();

return false;

}

Permission Permission

{

get: { return p; }

}

private Permission p;

}

Page 24: Unit testing legacy code

Step 3: Factory● Create a factory

● Have it return the new implementation

● Unless directed to return the wrapped old one

class PrivilegeFactory

{

IPrivilege Create(bool old=true)

{

if(!old)

{

return new Privilege();

}

return new LegacyPermission();

}

}

Page 25: Unit testing legacy code

Step 4: Test compatibility● Write tests

● Run all tests against both implementations

● Iterate until the new implementation has a satisfactory level of backwards compatibility

● This can be committed gradually

[TestCase(true)]

[TestCase(false)]

void HasRole(bool old)

{

// given

var f = new PrivilegeFactory();

var p = f.Create(old);

// when

var b = p.HasRole(Role.Admin);

// then

Assert.That(b, Is.True);

}

Page 26: Unit testing legacy code

Step 5: Dumb migration● Replace all uses of the old

implementation with the new wrapper

● Immediately use the exposed old implementation

● This can be committed gradually

void SomeController()

{

var priv = f.Create(true)

as LegacyPermission;

var p = priv.Permission;

if (p.IsAdmin())

{

...

}

}

Page 27: Unit testing legacy code

Step 6: Actual migration● Rewrite code to use the

new implementation instead of the exposed old implementation

● This can be committed gradually

void SomeController()

{

var p = f.Create(true);

if (p.HasRole(Role.Admin)

{

...

}

}

Page 28: Unit testing legacy code

Step 7: Verify migration is code complete● Delete the property

exposing the old implementation

● Go back to previous step if the code does not compile

● Note: at this point the code is still using the old implementation everywhere!

class LegacyPermission : IPrivilege

{

...

// Permission Permission

// {

// get: { return p; }

// }

private Permission p;

}

Page 29: Unit testing legacy code

Step 8: Verify migration works● Allow QA to explicitly switch

to the new implementation

● We now have a Feature Toggle

● Do thorough exploratory testing with the new implementation

● If unintented behavior is found, go back to step 4 and add a new test that fails for this reason, fix the issue and repeat

class PrivilegeFactory

{

IPrivilege Create(bool old=true)

{

var UseNew = %UseNew%;

if(!old || UseNew)

{

return new Privilege();

}

return new LegacyPermission();

}

}

Page 30: Unit testing legacy code

Step 9: Complete migration● Always use the new

implementation

● Mark the old implementation as Obsolete to prevent new usages

class PrivilegeFactory

{

IPrivilege Create()

{

return new Privilege();

}

}

[Obsolete]

class Permission

{

...

}

Page 31: Unit testing legacy code

Step 10: Clean up● After proper validation in

production, delete the old implementation