Unit-Testing und Refactoring sind zwei wichtige Disziplinen, die im Kern der heutigen professionellen Softwareentwicklung liegen. Sie ermöglichen es, den Code gegenüber Änderungen abzusichern und in einer guten Form zu halten.
Manchmal werden diese Disziplinen jedoch als zweitrangig betrachtet und vernachlässigt. Das Ergebnis der "eigentlichen Entwicklung" ist stark-gekoppelter Code ohne Tests. Solch ein Code, auch wenn er vor kurzem geschrieben wurde, wird von Software-Craftsmanship Pionieren wie Michael Feathers und Robert C. Martin als Legacy betrachtet und zum Verrotten vorbestimmt. Denn dem fehlt eine gute Test-Suite, die das Degenerieren verhindert.
Hier ist die oft empfohlene Strategie, den Code zuerst mit Tests abzudecken und erst dann Änderungen und Refactorings einzubringen. Bei der Arbeit mit Legacy Code ist jedoch die nachträgliche Abdeckung mit Tests kein leichtes Ziel. Exzessive Abhängigkeiten zwischen den Klassen, die wegen der Abwesenheit von Unit-Testing bei der "eigentlichen Entwicklung" unbemerkt entstanden sind, machen ein isoliertes Testen der Klassen unmöglich.
Oftmals steht man vor dem von Refactoring-Dilemma, welches Michael Feathers in seinem Buch "Working Effectively with Legacy Code" beschrieben hat -- um sicher zu refaktorisieren, braucht man Tests, aber um überhaupt irgendwelche Tests schreiben zu können, muss der Code vorher durch Refactoring testbar gemacht werden.
Wenn man sich dafür entscheidet, den Code mit Tests abzudecken, in der Form wie er, dann steht man vor einigen subobtimalen Optionen. Die exzessiven Abhängigkeiten verhindern isolierte Unit-Tests oder ermöglichen nur sehr schlechte Unit-Tests. Man muss dann entweder auf Integrationstests ausweichen oder "Power-Mocking"-Frameworks einsetzen, welche die schlechten Strukturen weiter zementieren.
In diesem interaktiven Workshop fokussieren wir uns auf Refactorings, die den Code testbar machen. Diese können gezielt für das nachträgliche Einbringen von Unit-Tests in legacy Code eingesetzt werden. Wir werden typische Situationen ausarbeiten und einige wichtige Aspekte vom objektorientierten Entwurf beleuchten, die für die Testbarkeit entscheidend sind.
Sources: https://github.com/rusio/refactoring-for-tests
2. 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
3. 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
4. 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
5. 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
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 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
9. 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
10. 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
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
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
14. 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
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 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
17. 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
20. 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
Notas do Editor
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.
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
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
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
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