Category Archives: Practices

TDD trick: getting effective & efficient design

How do we design a complex behavior or build a complex solution? Can we imagine everything big up front and then just write the full code?

 No, probably we will do it incrementally.

We will make progress incrementally and directly to the solution?

Well … it is complicated…

Just tell me how it works.

We will have a first idea of the solution, we will make it work, we will do some tests… and then we will continue with the next increments, but … we will often discover that we need to adapt and change the first code.

There is any danger to be stuck or have some other problems?

Yes, there are more sources of problems. First, if it is too complicated, we can be stuck from the beginning and we can have no idea how to start. Second … when we adapt/change the initial code to add the increments, we can break the behavior previously implemented.

What you say is that when we build something, what we really do is build & change? …and for any change we will need regression test?

Right!

Build real model: Build & Change & Regression Test. Repeat.

You are using test-first approach and auto-tests?

No…

… that mean, in this scenario (without auto-test), the real test will be rather performed when implementation is “ready”?

Yes…. but the ugly part is that we could have many broken parts, and we need to perform again all the tests. Even worse: what we consider as “implemented” will be a big fragile mess, hard to be stabilized.

That mean that, in fact, you will do a lot of tests, you will re-build and repeat many of the tests, with a lot of extra effort and with rather low quality as result?

Something like that.

Summary –  designing without auto-tests scenario

  • Facing the complexity, we can be stuck from the beginning
  • Design it is incremental anyway: build ~ build & change and repeat
  • With any change, we will break things
  • We will test a lot anyway
  • If we will repeat the test after each increment – that will take a lot
  • If we will postpone regression test until “implementation end”, we will still test a lot and we will get a big, fragile, buggy mess, very hard to stabilize
  • Design improvement is expensive and we will need again the whole spiral of tests
  • It is very likely to still have a mess after ”stabilization” and many hidden bugs

So … auto-test will fix all these problems?

Not necessary, you will need some tricks… as Uncle Bob “Getting unstuck”.

Robert C. Martin, Clean Code videos – Episode 19, Advanced TDD part 1, Chapter “Getting unstuck”: <<…and will write the most degenerate tests cases we can, we will very gradually climb the ladder of complexity on little test case at the time and for every test case it fails, will make it pass by generalize the production code, rather than doing some specific fix just to get the test to pass.>>   

What is happening here:

  • “Getting unstuck”
    • We will not be stuck from the beginning, because we can start with simplest test cases
    • It is less likely to be stuck later, because by extracting one test cases after another, the complexity of the remaining problems will decrease step by step
  • With regression tests available, we are free to adapt and advance to the solution.
  • Regression test is inexpensive and fast
  • Not breaking things when adding more features
  • No big fragile mess at the end

So… that should be enough?

No…. there are also other tricks, such Kent Beck: make it work, make it right, make it fast. You do not want to have a wonderful, fast & clean code and later discover that it is not working.

Summary of the differences  

  • Getting stuck versus Getting Unstuck
  • Throw away the tests with slow re-test versus persistent test with fats re-test
  • Break the previous increments versus keeping them working
  • Fragile big mess versus robust clean code

Manual tests ~ throw away the tests + re-build them for each regression test

 

    

Refactoring: strategy & collaborative work

Introduction –  The extended definition of Refactoring contains also this part:  “Its heart is a series of small behavior preserving transformations. Each transformation (called a “refactoring”) does little, but a sequence of transformations can produce a significant restructuring. Since each refactoring is small, it’s less likely to go wrong. The system is kept fully working after each small refactoring, reducing the chances that a system can get seriously broken during the restructuring.”- Martin Fowler, at refactoring.com.

Note: this imaginary dialog it’s inspired from an intensive and extensive practice.

What if we will have to perform a Big Refactoring? 

Refactoring cannot be big, it is a special kind of redesign, performed in small steps. See the above definition again.

Reformulate: I need to do a lot of refactorings to clean a legacy code.  There is any best practice?    

You need a strategy?

Yes!

Well, agile and software engineering offer a lot of tactics, including the Martin Fowler refactoring sets but almost no strategy, except … you should work clean from the first time. Use Martin Fowler indications from the start (or early enough), also Uncle Bob Clean Code and Clean Architecture.

Hey! I already have a lot of legacy code debt to solve!

Ok! Let’s build a strategy: how do we start?

This was my question!

Refactoring supposed improving the design, while preserving the functionality. Tests included. Do you have good requirements specifications or a good set of automated tests?

Not in this case.

Then you should recover functionality knowledge from the code and put/perform incrementally some tests. Better: functionality should be explicitly represented in the code and should be testable. And remember: there are two kinds of functionality…

Two?

Yes, first, the one that is application-independent and represent the target domain (domain business rules) and the one that is application-depend aka flow of control.

I remember: that sound like Uncle Bob Clean Architecture.

Yes. You will need to be able to apply distinct tests to them, without mix them with other concerns such as UI, persistence, network and others. Anyway, where do I usually start? I will try to make the running scenarios very clear into the code and that mean the flow of control.

In English, please?

I want to clearly see these: were the event triggered by the system actors start and the end-to-end full path until return. More, I want to refactor to make this path clear enough.

How could be not clear? ­­­

Global context. If the functionality path chaotically accesses the global context, then we could have undesired intersections with other paths/scenarios, that will compromise both data and function. In the same time, we can decouple flow/orchestration from specialized concerns.

What we get?

We will have explicit representation of the functionality (with no undesired contacts with other flows), needed for tests (we can apply auto-tests on it). Also we will have the first entry points to the specialized parts that also could be <decorated> with some tests. Then we can apply tactical refactoring as we need.

And …the domain business rules?

Must be decoupled from other concerns and you have to dedicate them specialized design/test elements.

That’s all?

Almost. You need to test any redesign. Tests need knowledge about functionality. If some parts are missing, now it is the time to recover them in auto-tests (preferable) or in other form of specification.

How do I know that recovered requirements are correct?

You don’t. More, you should always suspect that spaghetti-like legacy code include many unobserved bugs. You should validate these functional requirements by intensive collaboration with your colleagues, with domain experts, customer and other stakeholders.

Do you have any idea about how to do that?    

Start with Pair Programming (refactor in pairs). Pairing is not enough, and you will probably need more people involved – use Model Storming: discuss the resulted functionality with more colleagues.

Model Storming?

Yes, it is an agile practice, part of Agile Modeling (and Disciplined Agile) and it was created to complement core practices from XP. Also, you should actively involve your stakeholders in validating the recovered functionality…. Active Stakeholder Participation, that it is another Agile Modeling recommended practices. And at the end you will have more free bonuses.

What bonuses?

Functionality it is easy to accurately read from code (seconds!) and your colleagues and your stakeholders will already have acquired the recovered functional knowledge.

Summary –  Refactoring for significant spaghetti legacy code need tests/testing. Usually, knowledge about functionality necessary for testing it is insufficient, so must be recovered from the code. An effective & proven way to do that is to apply Clean Architecture principles: decuple both domain rules and application specific flow of control (aka use cases). Anyway, legacy code with too much technical debt will contain a lot of bugs, so recovered functionality it is inaccurate and need to be validated.  Knowledge & expertise needed for validation it is distributed among team members, domain experts, customers and other stakeholders, so you need to work in a collaborative manner with all mentioned parts. There are some outstanding software engineering and agile practices that could help on this aspect:

Note: “need” and “necessary” are often use in above text, just because we have followed the logical path of necessary things for testing a redesigned legacy code.

Remember: A lot of technical debt ~ inaccurate functionality. To refactor & test, you must re-start the process & collaborative work from functional requirements acquisition.     

Limits of inspected-in quality

 “All necessary test cases, starting from requirements”

Quality has two major “sources”: built-in quality (build without defects) and inspected-in quality (test, find defects and fix them).

Poor built-in quality is – statistically speaking – a known and common problem in software development. In many cases, with product growing,  quality will decrease in time by accumulation of technical debt. There is a wide spread belief that we can improve the quality (even in difficult cases) based mostly on test & fix approach.  We will try to prove that is “mathematically” wrong and it is rather a wishful thinking.

(Traditional voice) We will test & fix, right? And we will do that in the most professional way. We will document very well the requirements. We will generate all the necessary test cases starting from requirements. We will use them to execute very professional tests and we will make all the significant need fixes for a good quality.

Yes, indeed, a professional way to generate the test cases will start from the requirements, will identify functional flows, will generate running scenarios and then the test cases considering input data, test conditions and expected results.

(Traditional) That should be great! Even TDD implement the test cases considering all these elements.

That is true, but there are difference: TDD offer something more. Anyway, you forgot something …

How to blow up traditional testing

Let consider a set of pretty complex set of functionality. The orthodox approach of testing will do that:

  • (We have good analysts and good testers)
  • Write good enough requirements
  • Extract scenarios paths from functional flows and states transitions
  • Generate test cases from scenarios considering also inputs, conditions and expected results
  • Run the tests, find defects and fix them

That could take longer, but finally we will reach the quality goals, right?

NO!

Please consider this extra-scenario: what if we have too much technical debt. Let see some consequences. (used numbers are examples).

Scenarios –  If the requirements logic generates 100 scenarios, poor designed code could physically have 600 or more. How is that possible? It is pretty easy by accessing, for example, an undesired global context. If my scenario will access a global context (even only one global variable…), that will multiply the number of real test cases because these data are changed in an unexpected way (not specified in the requirements) by other flows. In fact, the global context will mix and multiplexes piece of functionalities that are not logically related. Duplicate code it is also a mighty enemy that create and multiply the test cases (“phantoms”, beyond the ones resulted from requirements). If we want to change a formula that was harcoded on each usage, how could really know a tester where was harcoded and how many times?

States – If the requirements logic suppose 50 states of the main entities & main logic, a poor designed code could physically have 500.  How is that possible? Again, that it is easy. For example, if the states transitions for one entity is not encapsulated, the code could be fragile enough to induce supplementary phantom states, because of poor written state transitions (one ways is to use only basic setters to realize these transitions).

Expected results – a poor design code will damage also the result display, log or persistence. The easiest way is to damage the displayed data: “catch” the errors and not display them, display a valid value for a null etc.

Let make a summary:

  • We have 400 official test cases for 100 scenarios and 50 states transitions and that cover the requirements
  • Physically, the poor designed system has more than 600 scenarios, 500 states transitions that will be covered by more than 2000 test cases

We will start testing – running 400 test case for a system that need 2000 test cases:

  • We will fix the defects that belong to the 400 official test cases
  • The testes will make some supplementary explorations and we will catch some of the defects from phantom test cases and phantom state transitions
  • A lot of defects will remain unobserved, mostly from those “undocumented” test cases

(Traditional) Wait!! The team – developers, testers and analyst will not discover the problem? We will test more and will fix more defects!

Based on experience, that will almost never happening! There is almost zero chance to generate all the “undocumented” test cases only by pure exploration: the tester has no clue about where are most of the hidden, phantom test cases.

(Traditional) There should be a way to solve that!

There is one: when a tester discovers some phantom test cases, when the expected results are damaged then that tester must report this quality problem: “we have test-damaging defects, please analyze them, and fix the root cause”.

Test and fix cannot protect you from test-damaging defects

(Traditional) That sound good enough!

It is not! It is too late to discover at testing that we have a such poor code, and a such poor build-in quality that will affect the tests itself and will cost too much.

Some conclusions

There is nothing wrong to generate in an orthodox manner the test cases from requirements. Just that you also physically implement these requirements, in order to make the generated test cases effective. The developer must not have the “liberty” to implement phantom scenarios, phantom states, to mix data and flows in a way that was not specified by the requirements.

We need to physically implement the requirement

It is not effective, inefficient, unethical and unprofessional to build a system that cannot be tested – where the real system test cases are much more that requirements-generated test cases (and are practically impossible to generate on testing time).

Traditional way of trying to get the quality mostly by test and fix it is many case a cognitive bias (inadequate logic), that bring a spiral of undesired results. The Martin Fowler Technical Debt Quadrant logic it is applicable in this case.

What could be done?

We need to reduce as much is possible the impedance between requirements specification and physical implementation. Some examples:

  • Physically “protect” the business. Separate the business aspects and do not duplicate
  • Basic design principles: do not duplicate, do not access global context
  • Physically “protect” the functional flows (separate: do not mix with other aspects)
  • Physically “protect” the logic of timing sequences (see Uncle Bob “Clean Coders” videos)
  • Physically “protect” the logical states transitions
  • Prevent and fix test-damaging defects

What about TDD?

TDD is in the list of things that help us to physically implement the requirements. The ultimate requirements are the test cases, were TDD will physically implement the test cases. The only major limit of TDD is that almost never will implement all the test cases.

The ultimate requirements are the test cases

… but we almost never implement all test cases in auto-tests.

Anyway, from industry experience, the examples from the above list of are needed also to enable the TDD.

New product and Minimum Viable Process with DAD

A live ecosystem

With a new product we will initiate and we must keep “alive” a full ecosystem (see previous post “A product … is not a product“) that involve:

  • business relationships
  • a viable process economics
  • sustainable technical solutions
  • a product team(s)
  • repeatable results
  • an evolving consumable solution and others.

What help me and my collaborators in some real cases were Disciplined Agile practices and some complementary ones such Clean Code and Clean Architecture.

“Temptations” and consequences

The first temptations on building new products were we have observed undesired consequences:

  • Copy-Paste Process: just repeat – without sufficient adaptions – the process from the existing products (more or less similar with the new one). The pressure could compromise more this process “re-usage”.
  • Ad hoc process:  If an existing process is not available, an ad hoc process is adopted without sufficient discipline and many process goals are neglected
  • “Normal” life-cycle: direct re-use a “normal” project life cycle, standard for the past projects/products
  • Ad hoc team: superficial building of product team, without sufficient collaboration and skills

The consequences could be very unpleasant:

  • A fragile product, where the poor design does not allow to reach the opportunities:  cannot deliver required changes, cannot offer a sufficient quality
  • The product is not consumable: not enough user level guidance, integration problems, performance problems
  • A significant mismatch between offered features and customers’ needs

 Different product – different ecosystem

We cannot re-use the process of a previous product as it is – the elements of the necessary ecosystem could vary more or less, and here are some elements that we found to be different:

  • The inherent complexity of business domain or of the solution
  • The customers & associated relationships
  • Derived from complexity: team skills, minimum viable good design
  • Required quality
  • Default performance
  • Others

As a first conclusion – we need to inspect and investigate the new context and adapt our process in order to build the proper ecosystem. Context investigation and adapting the process must be some permanent concerns, but the expected big deviation introduced by a new product must tell us that this is a special case to be considered.

 Risks, incertitude and opportunities

New product means a higher degree of risks, incertitude, but also possible opportunities. Risks and incertitude must be addressed in order to protect the opportunities and in the same time we need to keep the process adaptive enough to respond to these opportunities.

The main question is what practices and approaches we will need in a such context? We have found some that works.

Incertitude adapted life-cycle

For a new product, it is less likely to have a good enough initial envisioning of the requirements (and of the solution).  The DAD option of exploratory lean startup life-cycle propose a more adaptive approach where initial idea could be repeatedly adjusted after getting business domain feedback about what we incrementally build.

Choosing the right process options

As I have mentioned, the process “deviation” from what we know could be significant. Anyway, the process goals will be similar, but is possible to be needed to choose some different options. DAD main logic was built exactly on this idea. Beyond life-cycle approach we can choose various options for goals such: technical strategy, prioritization stakeholder’s needs, requirements elicitation methods, prove architecture early, validate release.

Some examples of options per goals described by DAD guidance:

  • Architectural spikes and/or end-to-end skeleton for proving the architecture early
  • Business values, Risks, dependencies as criteria for prioritization stakeholder’s needs
  • Coaching, mentoring, training, pair programming for Improving team members skills

When we begin a new product: best time to start adapting our options to the new context.

Effective practices

Building a new product it is difficult endeavor from many points of views: requirements clarification, solution design, (new) knowledge management. Here some key approaches and associated practices that we have found to be successful/helpful.

Effective/Efficient collaboration – apply Non-solo Development for modeling (Model Storming) and programming (Pair Programming). Active Stakeholders Participation.

  • Non-Solo Work it is critical for high difficult tasks related with initiating a new product
  • Because knowledge is just newly created we need to efficient/effective distribute that knowledge to the other team members and to the stakeholders

Opportunistic envisioning – Look Ahead

  • We cannot solve complexity and incertitude only with normal milestone-based forms of looking ahead – Inception Envisioning and Iteration Modeling. It was very useful to opportunistically use Look Ahead Modeling.

Knowledge evolution – Document Continuously, Document Late

  • Knowledge is just created – we have suffered when we forgot to capture, at least the essential – Document Continuously help us on this aspect
  • From time to time, we have extracted later some overview information (System Metaphor, Architecture Handbook), when our view and results has been stabilized – Document Late

Adaptive process –now is the best time to do that

  • The process was rather fluid and was defined/clarified as we advanced and saw what really works in the new context.
  • Good options were demonstrated by
  • Do Just barely good enough and refine not only your work, but also your process in rolling wave. Use events as architecture envisioning, look ahead modeling, iteration modeling to adapt your process

Minimal good design

When a new product was started, before and after the Minimum Viable Product, we were forced to keep change the product quick and with a sustainable pace. In order to “chase” the opportunities, we had to pay legacy technical debt and avoid as much is possible a new one. A minimal good design, it is necessary to deal with this continuous changes in the case of context complexity.  In this case, Refactoring,  Clean Code, Clean Architecture practices were the backbone for an agile, adaptive design and finally of an adaptive product.

Each distinct product will need a specific Minimal Good Design!

Some conclusions

DAD is built to offer context-adapting guidance. Using Disciplined Agile and outstanding practices as Clean Code and Clean Architecture it was be what we need to build a new product and to define the Minimum Viable Process necessary for that.

DAD and Agile Modeling practices are described here.

Bonus!

%d bloggers like this: