Refactoring for Testability
Techniken für Post-Factum
Abdeckug mit Tests
Rusi Filipov, 2014
Softwerkskammer Karlsruhe
Agenda
●
Legacy Code und das Refactoring Dilemma
●
Coding Dojo mit Refactoring-Aufgaben
●
Typische Situationen analysieren
●
Typische Refactorings anwenden
●
Ziel: Gute Unit Tests in Legacy Code
Rusi Filipov Softwerkskammer Karlsruhe 2014
The First Step in Refactoring
Whenever I do refactoring, the first step is
always the same. I need to build a solid set of
tests for that section of code. The tests are
essential because even though I follow
refactorings structured to avoid most of the
opportunities for introducing bugs, I'm still
human and still make mistakes. Thus I need
solid tests.
Martin Fowler, 1999
Rusi Filipov Softwerkskammer Karlsruhe 2014
The First Step in Refactoring
As we do the refactoring, we will lean on the
tests. I'm going to be relying on the tests to tell
me whether I introduce a bug. It is essential for
refactoring that you have good tests. It's worth
spending the time to build the tests, because the
tests give you the security you need to change
the program later. This is such an important
part of refactoring....
Martin Fowler, 1999
Rusi Filipov Softwerkskammer Karlsruhe 2014
Die Legacy Code Challenge
But the special problem of legacy code is that it
was never designed to be testable. -Péter Török
Rusi Filipov Softwerkskammer Karlsruhe 2014
Legacy Code – Definition
In the industry, legacy code is often used as a
slang term for difficult to change code that we
don't understand. But over years of working with
teams I've arrived at a different definition.
To me, legacy code is simply code without tests.
I've gotten some grief for this definition. What do
tests have to do with whether code is bad? To me,
the answer is straightforward...
Michael Feathers, 2004
Rusi Filipov Softwerkskammer Karlsruhe 2014
Legacy Code – Definition
Code without tests is bad code. It doesn't matter
how well written it is; it doesn't matter how pretty
or object-oriented or well-encapsulated it is.
With tests, we can change the behavior of our code
quickly and verifiably. Without them, we really
don't know if our code is getting better or worse.
Michael Feathers, 2004
Rusi Filipov Softwerkskammer Karlsruhe 2014
Test Coverings and Dependencies
Example: InvoiceUpdateResponder
When classes depend directly on things that are
hard to use in a test, they are hard to modify and
hard to work with.
Dependency is one of the most critical problems
in software development. Much legacy code work
involves breaking dependencies, so that change
can be easier.
Michael Feathers, 2004
Rusi Filipov Softwerkskammer Karlsruhe 2014
The Legacy Code Dilemma
When we change code, we should have tests in
place.
To put tests in place, we often have to change
code.
Michael Feathers, 2004
Rusi Filipov Softwerkskammer Karlsruhe 2014
The Legacy Code Change Algorithm
1. Identify change points.
2. Find test points.
3. Break dependencies.
4. Write tests.
5. Make changes and refactor.
Michael Feathers, 2004
Rusi Filipov Softwerkskammer Karlsruhe 2014
Unit Tests – Definition
Unit tests run fast. If they don't run fast, they aren't unit
tests. Other kinds of tests often masquerade as unit
tests. A test is not a unit test if:
●
It talks to a database
●
It communicates across a network
●
It touches the file system
●
You have to do special things to your environment
(such as editing configuration files) to run it.
Michael Feathers, 2004
Rusi Filipov Softwerkskammer Karlsruhe 2014
Unit Tests – Definition
Unit Tests are F.I.R.S.T.
●
Fast
●
Isolated
●
Repeatable
●
Self-verifying
●
Timely
Tim Ottinger, Jeff Langr
Rusi Filipov Softwerkskammer Karlsruhe 2014
Coupling: Static Dependencies
Object Peer Stereotype: Collaborator
●
Object with logic and behavior that we use
●
In test: replace collaborators with mocks
●
Inject mocked collaborators in SUT
●
Note: not all kinds of objects are Collaborators
Rusi Filipov Softwerkskammer Karlsruhe 2014
Refactor: Static Dependencies
Pass Collaborators „from Above“
● Avoid singletons and creating new collaborators
●
Accept collaborators via the constructor
●
Who should create the collaborators?
●
Parent object, main module, dependency injector
●
What about indirect collaborators?
Rusi Filipov Softwerkskammer Karlsruhe 2014
Coupling: Dynamic Dependencies
Situation: not possible to create collaborator at
construction time during object-wiring
●
Not enough initial information to create collaborator
●
Information available only after the SUT gets active
●
Must create collaborator dynamically after wiring
●
And still replace it with mock object in the test
Rusi Filipov Softwerkskammer Karlsruhe 2014
Distraction: Doing Too Much
Situation: a class is overloaded with many
responsibilities that prevent good testing
●
Class is not focused to do one thing
●
Instead: eierlegende Wollmilchsau
=> Class is harder to understand
=> Class has higher bug probability
=> Too many combinations of „features“ to test
Rusi Filipov Softwerkskammer Karlsruhe 2014
Refactoring Challenge: FtpClient
Evolution of a „Feature-Rich“ FTP Client
●
Core operations: list and download remote files
●
Extra gem: verify checksum of downloads
●
Extra gem: cache results from listings
●
Extra gem: reconnect if connection fails
●
New requirement: add ability to poll multiple
mirrored servers, so that the fastest one gets
used
Rusi Filipov Softwerkskammer Karlsruhe 2014
FtpClient: Original
Rusi Filipov Softwerkskammer Karlsruhe 2014
FtpClient: Refactored
Rusi Filipov Softwerkskammer Karlsruhe 2014
References and Code
●
Martin Fowler: Refactoring Improving the Design
of Existing Code, 1999
●
Michael Feathers: Working Effectively with Legacy
Code, 2004
●
Steve Freeman and Nat Pryce: Growing Object-
Oriented Software Guided by Tests, 2009
●
Source Code on GitHub
https://github.com/rusio/refactoring-for-tests
Rusi Filipov Softwerkskammer Karlsruhe 2014

Refactoring for Testability

  • 1.
    Refactoring for Testability Technikenfür Post-Factum Abdeckug mit Tests Rusi Filipov, 2014 Softwerkskammer Karlsruhe
  • 2.
    Agenda ● Legacy Code unddas Refactoring Dilemma ● Coding Dojo mit Refactoring-Aufgaben ● Typische Situationen analysieren ● Typische Refactorings anwenden ● Ziel: Gute Unit Tests in Legacy Code Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 3.
    The First Stepin Refactoring Whenever I do refactoring, the first step is always the same. I need to build a solid set of tests for that section of code. The tests are essential because even though I follow refactorings structured to avoid most of the opportunities for introducing bugs, I'm still human and still make mistakes. Thus I need solid tests. Martin Fowler, 1999 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 4.
    The First Stepin Refactoring As we do the refactoring, we will lean on the tests. I'm going to be relying on the tests to tell me whether I introduce a bug. It is essential for refactoring that you have good tests. It's worth spending the time to build the tests, because the tests give you the security you need to change the program later. This is such an important part of refactoring.... Martin Fowler, 1999 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 5.
    Die Legacy CodeChallenge But the special problem of legacy code is that it was never designed to be testable. -Péter Török Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 6.
    Legacy Code –Definition In the industry, legacy code is often used as a slang term for difficult to change code that we don't understand. But over years of working with teams I've arrived at a different definition. To me, legacy code is simply code without tests. I've gotten some grief for this definition. What do tests have to do with whether code is bad? To me, the answer is straightforward... Michael Feathers, 2004 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 7.
    Legacy Code –Definition Code without tests is bad code. It doesn't matter how well written it is; it doesn't matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don't know if our code is getting better or worse. Michael Feathers, 2004 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 8.
    Test Coverings andDependencies Example: InvoiceUpdateResponder When classes depend directly on things that are hard to use in a test, they are hard to modify and hard to work with. Dependency is one of the most critical problems in software development. Much legacy code work involves breaking dependencies, so that change can be easier. Michael Feathers, 2004 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 9.
    The Legacy CodeDilemma When we change code, we should have tests in place. To put tests in place, we often have to change code. Michael Feathers, 2004 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 10.
    The Legacy CodeChange Algorithm 1. Identify change points. 2. Find test points. 3. Break dependencies. 4. Write tests. 5. Make changes and refactor. Michael Feathers, 2004 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 11.
    Unit Tests –Definition Unit tests run fast. If they don't run fast, they aren't unit tests. Other kinds of tests often masquerade as unit tests. A test is not a unit test if: ● It talks to a database ● It communicates across a network ● It touches the file system ● You have to do special things to your environment (such as editing configuration files) to run it. Michael Feathers, 2004 Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 12.
    Unit Tests –Definition Unit Tests are F.I.R.S.T. ● Fast ● Isolated ● Repeatable ● Self-verifying ● Timely Tim Ottinger, Jeff Langr Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 13.
    Coupling: Static Dependencies ObjectPeer Stereotype: Collaborator ● Object with logic and behavior that we use ● In test: replace collaborators with mocks ● Inject mocked collaborators in SUT ● Note: not all kinds of objects are Collaborators Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 14.
    Refactor: Static Dependencies PassCollaborators „from Above“ ● Avoid singletons and creating new collaborators ● Accept collaborators via the constructor ● Who should create the collaborators? ● Parent object, main module, dependency injector ● What about indirect collaborators? Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 15.
    Coupling: Dynamic Dependencies Situation:not possible to create collaborator at construction time during object-wiring ● Not enough initial information to create collaborator ● Information available only after the SUT gets active ● Must create collaborator dynamically after wiring ● And still replace it with mock object in the test Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 16.
    Distraction: Doing TooMuch Situation: a class is overloaded with many responsibilities that prevent good testing ● Class is not focused to do one thing ● Instead: eierlegende Wollmilchsau => Class is harder to understand => Class has higher bug probability => Too many combinations of „features“ to test Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 17.
    Refactoring Challenge: FtpClient Evolutionof a „Feature-Rich“ FTP Client ● Core operations: list and download remote files ● Extra gem: verify checksum of downloads ● Extra gem: cache results from listings ● Extra gem: reconnect if connection fails ● New requirement: add ability to poll multiple mirrored servers, so that the fastest one gets used Rusi Filipov Softwerkskammer Karlsruhe 2014
  • 18.
    FtpClient: Original Rusi FilipovSoftwerkskammer Karlsruhe 2014
  • 19.
    FtpClient: Refactored Rusi FilipovSoftwerkskammer Karlsruhe 2014
  • 20.
    References and Code ● MartinFowler: Refactoring Improving the Design of Existing Code, 1999 ● Michael Feathers: Working Effectively with Legacy Code, 2004 ● Steve Freeman and Nat Pryce: Growing Object- Oriented Software Guided by Tests, 2009 ● Source Code on GitHub https://github.com/rusio/refactoring-for-tests Rusi Filipov Softwerkskammer Karlsruhe 2014

Editor's Notes

  • #15 Collaborator Lifetime: Was heisst es für den Lifecycle des Kollaborators wenn man ihn über den Konstruktor eingereicht bekommt? - Der Kollaborator muss vor dem Objekt geboren werden und muss mind. solange leben wie das Objekt selbst lebt.
  • #18 Don't Implement This With a Single Class!! Single Class = Higher Chance of Buggy Implementation Single Class = Unit-Tests are Doomed Must Extract Secondary Responsibilities into Separate Classes And Test only the Primary Responsibility of Each Class
  • #19 Don't Implement This With a Single Class!! Single Class = Higher Chance of Buggy Implementation Single Class = Unit-Tests are Doomed Must Extract Secondary Responsibilities into Separate Classes And Test only the Primary Responsibility of Each Class
  • #20 Don't Implement This With a Single Class!! Single Class = Higher Chance of Buggy Implementation Single Class = Unit-Tests are Doomed Must Extract Secondary Responsibilities into Separate Classes And Test only the Primary Responsibility of Each Class
  • #21 Don't Implement This With a Single Class!! Single Class = Higher Chance of Buggy Implementation Single Class = Unit-Tests are Doomed Must Extract Secondary Responsibilities into Separate Classes And Test only the Primary Responsibility of Each Class