Test Driven DESIGN - Step by Step
HTML-код
- Опубликовано: 8 сен 2024
- Test Driven Development is not really about testing, it is about design, and a particularly powerful form of evolutionary design. This allows us to make progress in small steps, taking fewer risks and evolving our system incrementally, learning as we go. But how do you do this for a real problem? This video demonstrates this incremental approach for a real-world, small project.
In this episode Dave Farley describes how to use TDD to build your system in a series of small steps, using a project of his own to demonstrate the approach.
Find Dave's Free TDD Tutorial (he refers to in this video) here ➡️ courses.cd.tra...
-
⭐ PATREON:
Join the Continuous Delivery community and access extra perks & content!
JOIN HERE for as little as £2 ➡️ bit.ly/Continu...
-
👕 T-SHIRTS:
A fan of the T-shirts I wear in my videos? Grab your own, at reduced prices EXCLUSIVE TO CONTINUOUS DELIVERY FOLLOWERS! Get money off the already reasonably priced t-shirts!
🔗 Check out their collection HERE: bit.ly/3vTkWy3
🚨 DON'T FORGET TO USE THIS DISCOUNT CODE: ContinuousDelivery
-
BOOKS:
📖 Dave’s NEW BOOK "Modern Software Engineering" is available as paperback, or kindle here ➡️ amzn.to/3DwdwT3
and NOW as an AUDIOBOOK available on iTunes, Amazon and Audible.
📖 The original, award-winning "Continuous Delivery" book by Dave Farley and Jez Humble ➡️ amzn.to/2WxRYmx
📖 "Continuous Delivery Pipelines" by Dave Farley
Paperback ➡️ amzn.to/3gIULlA
ebook version ➡️ leanpub.com/cd...
NOTE: If you click on one of the Amazon Affiliate links and buy the book, Continuous Delivery Ltd. will get a small fee for the recommendation with NO increase in cost to you.
-
CHANNEL SPONSORS:
Equal Experts is a product software development consultancy with a network of over 1,000 experienced technology consultants globally. They increase the pace of innovation by using modern software engineering practices that embrace Continuous Delivery, Security, and Operability from the outset ➡️ bit.ly/3ASy8n0
Sleuth is the #1 most accurate and actionable DORA metrics tracker for improving engineering efficiency. Sleuth models your entire development cycle by integrating with the tools you already invest in. You get a full and accurate view of your deployments, see where true bottlenecks lie, and keep your team’s unique processes and workflows. With accurate data, Sleuth surfaces insights that your engineers can act on to improve - with real impact. ➡️ www.sleuth.io/
IcePanel is a collaborative diagramming tool to align software engineering and product teams on technical decisions across the business. Create an interactive map of your software systems and give your teams full context about how things work now and in the future. ➡️ u.icepanel.io/1...
Tricentis is an AI-powered platform helping you to deliver digital innovation faster and with less risk by providing a fundamentally better approach to test automation. Discover the power of continuous testing with Tricentis. ➡️ bit.ly/Tricent...
TransFICC provides low-latency connectivity, automated trading workflows and e-trading systems for Fixed Income and Derivatives. TransFICC resolves the issue of market fragmentation by providing banks and asset managers with a unified low-latency, robust and scalable API, which provides connectivity to multiple trading venues while supporting numerous complex workflows across asset classes such as Rates and Credit Bonds, Repos, Mortgage-Backed Securities and Interest Rate Swaps ➡️ transficc.com
If you are intrigued by the MSEC project I refer to in this video, you can see simple demo here: ➡ msec-demo.continuous-delivery.co.uk
This now includes 300 wiki-style pages of information and advice for software engineers!
My favorite part of TDD is the built in reduction in compile/run time compared to working on an entire application or system. Going from getting feedback in minutes or even hours down to seconds is a complete game changer.
When I first tried automated tests, I was coding first and testing after and saying "I'm doing TDD"... I was testing "units" that had far too many responsibilities and found it to be quite a struggle. This was when my code started to get a lot more "SOLID" because SOLID code is so much easier to unit test than the (not huge but still) too large classes and methods I had been writing.
Once I finally got a handle on what TDD REALLY is, those incremental steps made my code come out far more "SOLID" almost as a by-product.
The funny side effect of all this is that I used to hate Python because of it's lack of delimiters at the end of a block... by as my code become more "SOLID" and I became a "never-nester" my need for end-of-block delimiters became far less necessary and I learned to love programming in Python.
The wiki idea for navigation is amazing! RUclips playlists are very limited for the expiration of information
Thanks, I am glad that you like the idea. It is a bit of an experiment, but one that we have been working on for a while.
Test requires an interface. So write the test requiring the interface. Now create the empty interface. Now add behavior to the test. Add that behavior spec to the interface. Now write the minimum implementation to satisfy the interface. Repeat every 10 seconds.
ZOMBIES - Zero, One, Many, Base, Interface, Exceptional, and Standard in regards to Cases you can have tests around, and assign specific tests to each letter of that acronym for CI/CD. So, applying this in your example adding Zero would be the first Case.
I really enjoy the exploratory nature of TDD, but sometimes I find it hard to make it fit into an existing structure; not because the project itself has a bad design, but because I get to a solution "of my own preference"; and sometimes that feels alien within that specfic structure, be it naming convention, folder structure or any other pre-established guidelines.
I think that the core is still correct, even if you loose the value of exploring the design. If you are writing to a fixed external interface, then you still want to focus on the behaviour that you want that interface to expose. Depending on the interface this may be less pleasant than when you have the freedom to change it when it is crap, but focusing on the behaviour still means that you don't care about implementation detail - which is the real win.
Hi Dave, If you decide to add new classes during your refactoring step, which is very likely, do you start a new TDD session for each of them or are they considered taken care of by the current tests?
I intend to give TDD a shot on my next dev, I ve been doing only CDD so far where we have to test every single class so it would feel awkward to do so with TDD it seems to me if I have to stop testing the current class to start another TDD session on a new one and then come back to the other one later.
Thanks, I enjoy you videos a lot.
If you are truely refactoring you don’t need to testdrive the new classes, you already have the code. You might want to add pinning tests for that functionality though. And if you want more functionality in them you should testdrive that, but take care to not drive more than you are sure you need. Also the needs from you first class might change as you go, so I tend to mock the new classes, at least till I feel confident about their ”api” and responsibility. Sometimes I keep the mocks until I’m ”done” with the first class and then turn my attention to the others. At that point I know what I need.
Here is how I understand this. The classes you create at the refactoring stage generally don’t require testing. You create tests for the logical boundary of the feature, make it work, and then refactor as much as needed to make the internal design of the feature good enough. After each refactoring step, the tests you’ve previously made will assure the functionality is not broken. You may end up with the single class or with ten classes. The beauty of this approach is that later when you get even better idea of how to structure you code, you can refactor the feature again and be confident that it still works.
You add tests only when a new requirement for the feature comes up. You change a test only when a requirement changes.
This approach encourages you to make changes in the system. In contrast to the situation when you’re reluctant to enhance the code because this would require rewriting tons of tests.
@@penaplaster It's a good point that if the new, extracted class is only part of the functionality of the first class you should avoid adding tests to it to create dependencies from your tests to your implementation details. If that extracted class becomes a first class citizen used from different places and possibly with additional functionality, you probably should.
@@thomasthoninilsson Another important aspect of this is that you should instantiate internal classes directly rather than getting instances from a DI container. Calls to internal classes should not be mocked (except when they call external dependencies). This again helps making tests insensitive to structural changes within the logical boundaries of the feature. Also, this promotes cohesion: you explicitly state that a certain class is an integral part of the feature.
It depends, I try to avoid allocating test cases by class, and more on areas of behaviour, so sometimes I can leave them where they are, sometimes it can make sense to move them to a new TrestCase.
I struggled to use TDD implementing quantitive finance model. Result of such model, even for simple cases, is result of multiple data transformation and mathematic computations, so it’s not so obvious. Even if I divided model for simpler subfunctions, still specifying result of computations (matrix operations, looking for minima’s) was hard. Inputs was based of output of previous functions. Also I had little experience in programming algebra operations with numpy, so it was also learning process for me. What I found that’s better works for me is programming interactively in jupyter notebook, where I could do little steps in adding code, checking and investigating results of single computations, which in my opinions didn’t deserve to be separate function, use result of one function as inputs of another, and then write tests at the end of the process for regression. Probably it is called REPL driven development. Maybe I couldn’t simplify problem enough to use TDD, maybe such programming is alternative for TDD?
Another example of such workflow is implementing pure presentational components in fronted. With tools like Storybook, you can implement single component looking of visual effects of your code while coding, and then implement snapshot tests for regression.
I say this, hopefully, to show others that I have 30 years of coding experience and dont use it, and nor do you have to.
If you enjoy it, fine, but dont feel pressured. It is not a silver bullet, it is not the best way to learn or become a designer, its not the best way to code. It is just one technique.
It has some rewards, but its not entirely a positive. It has negatives especially for people who have experience.
Lets say you are asked to build an SSO. Then in your next company you are asked again. You then participate and find issues in open standards and you have spent a lot of time understanding the nuances of security and functionality. Now you are tasked with building your third SSO, together with two other people with more experience than you. Are you honestly going to start with a little test to verify a token and then a little test to format a url, then a little test to add a user. No, you are going to MODEL to the entire SSO and you are going to go directly to the areas of your design that you want more understanding or that makes this new architecture work best. Here you will build a prototype to verify your assumptions. You will then jump to other platforms and test this design choice in other languages. You are not going to rewrite the test for formatting a token in the other language all over again.
Challenges that block me from progress are when we already have a system with bugs and/or edge cases that sometimes occur. How would you tackle this using TDD? Recreate the bug using a test? other? interested in your take/approach...
I guess it depends on why the bugs are there and what you mean by edge-cases. TDD is a design technique more than anything, so this is at its best in helping to create new good code without bugs. Edge-cases are fine, they are just new behaviours that the code needs to deal with, so write a test for those behaviours too!
TDD can still help with bugs though, when a new bug is found, write a test that fails because the bug is there, then make the test pass - so eliminating the bug, and demonstrating in the process that you have.
@@ContinuousDelivery example edge case: AWS lambda environment. usually a fetch request to a vendor api responds within 50ms. but on occasion, if it doesnt respond within 3secs, the request is retried. This causes typical concurrent modification and duplicates request errors as the previous request actually responds albeit eventually. Trying to recreate those edge cases have proved tricky using TDD in a lambda domain... hope that helps...
Hey Dave, like your channel and the content. Just wanna give a little feedback: the thumbnail for this video and the last one feels quite creepy to me. Usually I can look past these but this one specifically … makes me quite uncomfortable to look at.
I agree and be on to the point it’s the photo / facial expression that looks… not hmm let’s call it “appealing”
Have also been thinking this! Seems like somewhere someone decided that warping/“touching up” people’s faces in the thumbnail drives clicks. Seems really out of character for this channel especially…
I think it would be funny if he went overboard and made it completely cartoony to the point of ridiculousness.
I for one, think the thumbnail is amazing.
@@Mozartenhimer I too couldn't see a problem whatsoever
I have adopted TDD as much as I could for about 3 years now. I'ts going great and I get the benefits you describe. Even when I don't write tests upfront, I still think about my code differently. One area where I seem to struggle a bit is, if I have to implement an interface with a specified behaviour. Right now for example I am trying to write my own Gymnasium.Env in Python and try to do it TDD style. Since the interface is set in stone already, I can write some degenerate cases for each method to start and implement those - but it just feels wrong, since I'm not really discovering a new solution to a problem, I am implementing rules that I already mathematically explored and know how they work. Should I still use TDD in this case? Sure, I'd test the behaviour with some value and property tests, but in the case where both the interface and the solution are known, does TDD still make sense?
As opposed to what? What other method are you proposing to use? Do you plan to just open up an editor and write straight-shot each file in your entire code base from top to bottom, and then run the whole thing and it will work perfectly the first time you run it?
Chances are you stop and test your code regularly. You're going to take small steps, write the code to implement each small step, and then execute your code to verify that what you wrote works. Why wouldn't you write those steps out as unit tests that will automatically run as you make changes and give you instant feedback at every moment? If you can come up with a faster or more effective feedback loop, by all means use that and please share it with us.
I've never understood the hullabaloo about TDD. It seems so logical to me: if you're writing code, it's because you're trying to accomplish something useful, so why not write out that useful thing to begin with, in as high a level as possible, as an executable mini-specification that will automatically run as you type code and provide you with instantaneous feedback about your progress?
The only exceptions I can think of is if you are not writing new code, but trying to make small adjustments to existing code to change a behavior, or you are trying to write your code by copy-pasting some huge chunk of code from somewhere else and then making modifications. In those cases, TDD will be hard to apply directly depending on whether the code has unit tests available or not (although even if unit tests are not present, I still follow a version of TDD with adjustments from Michael Feather's book).
@@fennecbesixdouze1794 Yeah, the alternative is to just write some tests at the interface (as I wrote) and then just implement the pre-defined design. This goes counter to what TDD provides, that's why I was wondering. It's very close to what you describe in your last paragraph actually, just that I don't exactly copy code but translate math equations into code. This is the first time in 3 years practicing TDD that following the discipline feels off. Not because I don't write any tests, but because the design is set in stone even before I wrote a test.
I think that you still want the tests to focus on the behaviour and not the implementation detail, even though you have lost the malleability of the interface. That then still means that your tests aren't coupled to any specific implementation details, which is good.
I think there is a gap between what you are saying and presenting here, or maybe am I simply very confused.
"A function adding fractions should return a fraction" is the thing I would like to see expressed in the unit test, because that's a property of the addition of fractions. I really don't understand why you test the result as a string You say that it is more readable but you also say that you want the test to reflect the interface, which it doesn't, in part because there seems to be magical type coercion going on. It's impossible to tell by looking at your test what the real return type is.
By doing this, you are also adding confusing factors in that the test is now testing a proxy and not the real thing that it needs to test, and that in order to test that proxy 2 things need to work (addition and rendering).
Now you could reply that if the return type is a fraction, that step could just as well fail independently of the addition, and it is true, but it's also the type of the input, so maybe, actually, the creation of the type could have been our first test (but what should it look like? Do you render it? It's turtles all the way down)
This is a problem I don't only have with TDD but with testing in general: helpful tests test few things at once, so that when they break they can point to a single cause of failure, and of course as your code grows in complexity, achieving this is less and less practical. This definitely contributes to cognitive load when writing tests, and maybe your approach is more pragmatic, but it looks a bit carefree in comparison.
In the case of fractions it's actually pretty simple to know what the next test should be: we could simply list all the mathematical properties of the addition of fractions, and actually it would be a better living documentation for our code because it would be one that makes sense, and someone familiar with the domain could point out that there are properties missing or whatnot, but of course this is harder to achieve when you don't know where you are going.
I know that your emphasis was on design rather than functionality here but the two are intertwined.
I don't disagree with your end-solution, that is where I end up too, but the *intentional process* of TDD is to make progress in small steps, allowing me to verify my progress often. If I chose to return the Fraction in the first step, I now have more work to do between tests, it is a choice, but certainly when teaching TDD, and mostly while practicing it, I prefer to make change more incrementally.
In this case, if I had chosen to return a fraction I would also need to implement a 'toString' method, and call it in the test, so unnecessary complexity for the first step IMO.
@@ContinuousDelivery I see what you mean. The alternative would have been for me I guess to implement an equals operator before implementing addition. The first test for addition could be the addition with 0 because returning oneself is simple and enables to test the 2 functionalities together for the first time. This equals operator would later deal with greatest common denominator (I'm not sure it should be the responsibility of addition to simplify fractions but equals does need to do it and using it enables to defer the decision of doing it in the add method).
I am clearly overthinking this though.
It's easy to overthink what to test next. It was a great topic for a video.
@@ApprendreSansNecessite Yes, and the "toString' vs 'equals' is a nice design choice to make, either is fine, but it is nice to proceed in small steps while you are making the choice.
I like the thoughts but this channel is too philosophical. I'd like to see more pragmatic examples from you. A code along video with examples of how and what to test would be awesome.
Please drop the word "Test" and start use "Specification" finally!
The assertion and act should be two different statements, segregated by at least a blank line. The way it is written now impedes readability. Also the test scenario name is poor - it doesnt't reveal what is expected. And you forgot to demo that in TDD, you can write a test without any code to begin with, red means does not compile and it can be fixed by generating code.
The name of this video is also deceiving - it is just one test case. What to test next remains unanswered.
Arrange-Act-Assert is a pattern, not a Law of Correct Unit Testing. For a simple case like adding two fractions, I much prefer inlining rather than having a separate statement assigning the "actual" to a temporary variable to assert on, simply because it is more concise, and it is perfectly readable.
Consider this code:
@Test
void aTimeSegmentCanBeInSeconds() throws ParseException {
assertThat(parser.parse("PT10S")).isEqualTo(Duration.ofSeconds(10));
}
This test is a one-liner, but to me, it is perfectly clear what is under test, and what is being asserted: the string "PT10S" gets parsed to a duration of 10 seconds. Now I could separate this into separate arrange, act, and assert statements like so:
@Test
void aTimeSegmentCanBeInSeconds() throws ParseException {
String tenSeconds = "PT10S";
Duration actual = parser.parse(tenSeconds);
assertThat(actual).isEqualTo(Duration.ofSeconds(10));
}
I strongly prefer the first version, simply because i can scan that piece of code to get the information i need much more quickly. In all honestly, if my test is such that i would need to split these statements up in order to improve readability (which can definitely happen), that to me is a smell that the test is too complex and that i either need to redesign that test, or redesign the code under test, in order to allow for better testability.
Key points from the video:
TDD allows for progress in small steps, taking fewer risks, and evolving a system incrementally.
TDD can help surface design problems, but it does not necessarily lead to good design. The main barrier to TDD is not testing, but rather exposure to bad design choices.
When learning TDD, it's important to focus on understanding the problem before trying to find a solution. Start with a simple example and create a test for it, rather than imagining a solution and trying to test it.
The speaker is designing an API and has made some design choices, such as using a type to represent fractions and adding them as fractions rather than strings or decimals. He believes it's important to make design choices early on and experiment with them while the code is still simple.
The speaker discusses his thought process for designing a fraction type in his code. He emphasizes the importance of not thinking too far ahead and instead focusing on organizing things in different ways.
The speaker is working on a project involving adding fractions and uses the test as a design tool, focusing on separating the concerns of addition and rendering fractions as strings.
The speaker discusses the design and organization of a parsing job, starting with a single page and progressing in small steps. He prefers modular code with a good separation of concerns and easy testability.
The speaker discusses his approach to using tests in TDD. He emphasizes the importance of testing recursion and creating circular links to ensure the recursion ends. He also mentions the importance of testing for pretty names and how tests can serve as mini specifications for the code.
Test driven development NEVER WORKS. NEVER, EVER, EVER EVER! It is not how people behave. Yes you can have good testing framework. But human creativity does not start with test. Face it. Fact it. Face it. You can spend next 20 years talking about test driven development. And 20 years later it will still the same: design, development and test. That is the natural order. Just like putting the underwear on first, then put the pants on, then put the shoes. Natural order. That is how human think and behave.
Of course in AI work, you can dictate how machines operate. And you can dictate test driven development. And it might work. For machines do not care to develop test cases first.
Tests describe how an interface should be used, and on the other side of the interface is a black box to the tests. By writing tests first, I've always come out with much cleaner interfaces and more decoupled code.
"That is the natural order" ... Says who?
Sorry but that is simply factually wrong. It works for every team I have worked on for the past 24 years, it works for SpaceX, Tesla, Microsoft (in parts), Google (in parts), Amazon, Netflix and so on and so on and so on. It is how some people behave, and when they do in my experience they write better software as a result. Human creativity starts with understanding something about the problem, and then making a guess about how you would like the external answer to be communicated to you - that is your test.
We could argue about the word "Test" in TDD, I think it is better thought of as a specification, you don't write specifications after the design & development do you?
@@manuelstausberg8923 Says FACTS. Tell me how many companies are doing test driven development. This approach has been pushed for so many years. But how many companies adopted this process?
@@pyrotecx123 I am not saying it does not benefit YOU. But people simply do not start writing test case first. If you were right, many companies have been doing it. But let's face it, most companies are still using design, development, unit test, integration test, qa test and deployment process. You can define an interface for your feature. But that is part of the design. It is not test.
bla bla bla bla bla
The site demo is down
MediaWiki internal error.
Original exception: [44233e5025c4c886d03f329d] / Wikimedia\Rdbms\DBConnectionError: Cannot access the database: php_network_getaddresses: getaddrinfo failed: Name or service not known (database) (database)
Backtrace:
Notably... The connection to the database is something tdd folks seem to feel is rarely needed....
Can you talk about ChatGPT and using AI tools for programming?
He has already.
@@fennecbesixdouze1794 where?