Checking session availability…
Hang tight while we load the latest updates.
Nix will talk about test, deploy and solving problems fast! He highlights why people should adopt a test-driven-development approach.
Shared understanding, shared ownership and a test-first approach leads to high quality software, trust and high performance.
Solving Problems Fast
Nix Crabtree at UXDX Community: London. Video: https://youtu.be/IpBXhOzoPsU
Readable transcript: edited from the recording's captions for readability (fillers and false starts removed, punctuation and section headings added). Wording is the speaker's own. Timestamps are positions in the video. Names marked [?] could not be verified against the audio.
From TDD to the big picture
[00:00:10] Okay, so it's a hard act to follow, because I loved all the stuff that Will [?] said. It was actually really, really good; it was all the stuff that I really enjoy about engineering. So hopefully I can complement that with how we can go about shortening feedback cycles, and how we can improve collaboration and testing and all that other good stuff, in the next 20 minutes.
[00:00:40] We are going to talk a little bit about TDD. The way we're going to work is a bit lighter. If you've seen these things on Facebook where it starts with an atom and then you end up at the universe, we're going to do that, but it's just not going to be as spectacular. We're going to start with TDD, then ATDD, and then go into the big picture. So it's not really atom to universe.
[00:01:05] We're going to get into the TDD cycle quickly. I'm guessing we haven't got a huge number of developers here. Are there any developers here? Okay, that's good. I'm going to talk about ATDD, acceptance test-driven development, which is really what the core of this is about: things like shared understanding, shared ownership and a test-first approach. I'll talk about the ATDD cycle, which sits around the TDD cycle, and then we're going to talk about the big picture. So who else is here? UX, product management? Not bad at all.
Why test-driven development
[00:01:50] Let's start small, with test-driven development. When we write code as engineers, what we're interested in is solving the problems that we're given by the business, the business requirements. What happens if you just start smashing in code is that it starts scaling out of control. TDD solves some problems.
[00:02:16] It's not clear exactly what needs to be done. You have an idea, you start doing some stuff: we might need some of that, maybe we do a bit of this. It's a little bit wild. It's not clear whether all the code you're writing meets a known requirement. Is there value in writing code if no one's asked you to do it? It's debatable.
[00:02:38] It's not clear whether the code base already meets that requirement, intentionally or otherwise. If you're writing code to meet a requirement, but actually it already does it, someone has already done it, another team has already done it, someone's done it historically but in a completely different way, you need to know that. Otherwise you're wasting valuable time when you could be developing features.
[00:02:59] Long feedback cycles. Ideally, and this is the crux of this whole talk, how do we shorten those feedback cycles? How do we know as soon as possible whether it's viable?
[00:03:12] And then documenting the code. You don't want to write comments in your code. Comments are a fungus. You change the code; do you change the comment? Maybe you do, maybe you don't. How do you know? You've no idea. You could read a thing that says, yes, this whole method does this, but actually the method's completely changed, maybe it's not even used, maybe it's deprecated, maybe it does something completely different. Developers are not well known for their meticulousness in documentation.
[00:03:43] Or maybe it's somewhere completely different. Maybe it's in Confluence or another kind of wiki, or maybe in OneNote, and you make a change, or you make it quickly, or you just break it, and you can't be asked to update the documentation. So then you end up with two completely different states. With test-driven development, your code documents what it does, because those tests run every time you make a change.
[00:04:11] The key side effect is that by going through a TDD process you implicitly create and maintain a regression suite. So every time you make a change, you know that you haven't broken anything, and you know that when you start, your code base doesn't already do that, by accident or otherwise, and when you finish, it does do it.
Red, green, refactor
[00:04:36] The TDD cycle; we'll go through this quite quickly. Red, green, refactor; you've probably heard the terms. You write a test that exercises the functionality you're going to implement, and then the test fails, it's red. If it's not red, your code base already does it, or something's gone wrong. You then make the smallest, simplest change, the smallest possible change, to make that test pass. That doesn't mean that you go off and spend three hours implementing stuff. You make the smallest possible change so the test passes, and it might simply be returning a literal value: your test says this should return two, so return two, that's it. The idea is you iterate over it.
[00:05:23] The test passes, it's green, that's good. You might then want to, as you go along, restructure your code. Not the behavior, just the way your code is written, to do exactly the same thing in a better way. That's what we call refactoring. And the unit test obviously should still pass, because if you've changed something and it fails, then you've changed the behavior, not just the way it looks. Cool, makes sense?
ATDD and where requirements break down
[00:05:48] ATDD, acceptance test-driven development, is a larger process that sits around TDD. TDD is a very developer-orientated approach. It's involved with the code, and it's basically about declaring what you're going to do before you go and do it, so that what you do is just what's required.
[00:06:18] With ATDD: the flow of requirements is sometimes a bit disconnected. I'm sure we've all seen this. We want this thing, and it goes to a vision document, and then it goes to a BA, and it goes to the development team, and they go, "Oh yeah, I think actually what we want is this thing," and those two things are off. Sometimes you can't take stories into a sprint. I'm going to talk quickly, so grab me afterwards.
[00:06:54] Poor requirements definition. We want a thing. What thing? We want it to be shiny. Excellent, what do you want it to do? Sometimes when you get to the point where you want to deliver this, you want to take it into a sprint and develop it, you can't, because you just don't know enough about it.
[00:07:08] Rework, when requirements aren't implemented as intended. You work on the thing and then someone looks at it and goes, "That's actually not what I was after at all." It's a waste of everyone's time.
[00:07:22] Acceptance criteria. We wanted it to do this. It's of variable quality. What you want is to know that what you've delivered is exactly what was asked for. Sometimes you need to translate these things, transform them from one format to another, and take a 17-page vision document into 17 individual functional tests, which takes a person to interpret.
[00:07:52] Collaboration between the three amigos: our product owner, or BA, or dedicated product person, whatever it might be; someone looking after the test concerns; and someone looking after the software development concerns. Sometimes it's inconsistent, and those three things are absolutely key.
[00:08:17] Multiple testing, longer feedback cycles. Ad hoc testing doesn't work. Second-class citizens: quite often QAs, BAs, UI/UX are second-class citizens in development teams. The software engineers are the ones who go, "Stand back, hold my pint, I'm going to go in and I'm going to do this, you're going to love it." It doesn't really work. Everyone knows those people.
[00:08:47] There are three key things that ATDD brings: shared understanding, shared ownership of what we do, and a test-first approach. Those three things give us, and this is the magic preachy bit, high-quality software. They give us high-trust teams and high-performing teams. It sounds very preachy, but it was actually true in my personal experience, having done this. It really works.
Shared ownership
[00:09:18] I'm going to tell you a little bit about shared ownership. No clocks; I'll go until someone pulls me off the stage. I'll talk a little bit about Scrum and agile. If you want to talk about development methodologies, come grab me afterwards. Scrum itself makes some very specific statements about ownership. It says that Scrum recognizes no titles for development team members. And the key thing here is not these particular rules. It's that a development team is a team of people, a group of people, whether they actually work on the same team, under the same line manager, or in the same building, whatever it is, even if you work across different companies.
[00:10:11] The idea is that you as a development team are developing something, a feature or product, whatever it might be, that ultimately is going to go to a customer, whether that's someone on the street or another business. Ultimately you are all developing the product, and you should all be 100% involved in it and 100% behind it. So everyone is developing it, regardless of what you have to do on that team.
[00:10:40] Everyone is developing. There are no sub-teams. You don't have a testing team. If you ever hear the term "we'll come to this," and someone says "it's dev complete now, there you go, go and test it then," it's not true. It's done when it's done. The individual development team members might have specialized skills, of course. You can't have everyone being a jack-of-all-trades; it doesn't work. But those people should all be first-class citizens within the team.
[00:11:15] There are no heroes, the "hold my pint, I'm going in" kind of person. The team is responsible, and that might be an extended team. That might be UI/UX, it might be product managers doing high-level vision stuff, it might be low-level deploying stuff in PowerShell. Everyone is developing that product and making something they can be proud of. They should feel a collective sense of pride. If you've been there, if you've felt that, you know what I mean. If you haven't, find a new job, or change your company [?], change the culture.
[00:11:53] There is no dev complete. Something on our product backlog, whatever it is, a feature, is done when it's proven to meet the acceptance criteria. Not when you finish writing the code, not when you as an engineer go, "Yep, done, checked it in, CI/CD is running, I'm going to do something else." It's not done. It's done when that team, extended or otherwise, is happy it's done.
[00:12:15] There are no handoffs in a high-performing team. You can't necessarily avoid all handoffs, but basically, if you can do everything within that team, there's no point at which you need to go, "I need to stop this, put it down, wait for someone else to go and do something before I can get back to it." Ideally, the fewer handoffs you have, the more high-performing your team. Think back to classic XP practices: you've got everyone you need at the same table, and you've been using the same keyboard sometimes.
[00:12:52] Then there are no second-class citizens in high-trust teams. Everyone on that team should feel that they contribute equally to whatever feature or product you're developing, and share that same amount of pride.
Shared understanding and ubiquitous language
[00:13:02] Shared understanding. I'm going to skip through this bit a little bit, because I've got about three hours of material and 20 minutes to deliver it. How do we make sure everyone understands what is actually required? What are we doing as a development team? Everyone on that development team, extended or otherwise, needs to understand it, so that when we've done it they can look at it and say, "Yeah, it's done, we love it, we're really proud of it."
[00:13:31] We tend to look at things like writing requirements in a common format, so that we all understand them. It means we don't have to interpret "we want a shiny" into "we want a control on a page which has a specific behavior when in this view or in our mobile view." Those two things are interpreted differently. What you want is the same thing that you can define, test, implement and test as you go through.
[00:14:00] We use a ubiquitous language. Ubiquitous language is a classic concept that's been around for literally decades, where we share the same terms. If I say to you, "We need to use the product," that's going to mean something different to every person in this room. For me, this is a product, but it is not a product that I can actually sell. A product means a group of variants, which come in sizes and colors. For someone else, a product is something they're developing. A minimum lovable product, for example, is a product of something.
[00:14:47] Using ubiquitous language means that we all understand straight away what we mean, and we don't interpret it wrong. So we keep a domain dictionary. We say, when we say "product," what we mean is this. Great. That allows us to incorporate it into our test automation, which means that we can say "product," but actually there's some code that says, okay, we're going to instantiate a product object, and it's going to have an ID and it's going to have a name and some other stuff, and you can start to build up the scenarios.
[00:15:20] And talk to people. It's so obvious. If you've ever been involved in the back and forth of email or Slack or Skype for Business, God forbid, or whatever it might be, where you're basically just throwing things over, when actually if you just walked over to that person and said, "I just need to know this thing: is it this or this? Because it changed last week." Done. Talk. Everyone understands, everyone feels good about it. There's so much less opportunity for hostility.
Given, when, then as executable requirements
[00:16:00] A common format that we can use for these things: given, when, then. I'm sure we've all seen this. Given this state, when I perform this action, then I see this result. Pretty simple. The key thing with ATDD is you can define your high-level requirements like this, and you can take those high-level requirements and actually turn them into executable tests with exactly this format, not interpreting them in any way. Actually taking this and putting it into SpecFlow, let's say, or writing your own framework.
[00:16:33] It says: I'm going to set up something. I'm going to create a customer, I'm going to log into that customer's account, and I have a logged-in customer. They are going to add a funky t-shirt to their bag, and then I should see that product in the bag. This is obvious stuff. It says nothing about implementation whatsoever. It's talking about a behavior.
[00:17:02] That behavior can go from there right down to the unit test. Going down to a unit test of this, I can say: okay, I need to instantiate something which represents a customer. They're going to have an ID and an order history, they're going to have some other stuff. When they add something to their bag, I'm going to instantiate a bag, or basket, okay, add a product of a particular ID in a particular variant, and associate it with that customer. Then I see this result: yes, it's in their bag and it's associated to that customer. I've taken that from a high-level requirement that just says "I want this to happen" to an actual executable requirement, a requirement I can execute again and again and again as I make changes to the code base, and everyone understands it because it's the same thing.
[00:17:49] To reiterate that: you see, a given-when-then is a logical function. It doesn't have to have any actual executable functionality. It's a logical function. If anyone studied symbolic logic at university, you'll understand. I'm beginning to believe I'm the only person who did. You provide some known input, you perform an action, and you expect some kind of known output. As simple as that, it's a logical function.
[00:18:22] The thing is that that is what a unit test is. We take some input, some state, we execute an action, and then we examine it, we assert an outcome. But so is an acceptance test. If we do something like SpecFlow, for example, then we're doing exactly the same thing; we just do it without understanding what's in the code. We're just automatically calling our code base. We're saying: do this thing based on this input, and what's the state afterwards? It's the same thing.
[00:18:52] Which means that a functional requirement is the same. Converting functional requirements into acceptance tests, into unit tests, into working software, can and should be done from exactly the same set of criteria. And I do mean the same words, the same everything. It works if everyone works as a team, extended or otherwise, and uses the same kind of language. Everyone is singing from the same hymn sheet; no one gets the words wrong and no one sings out of tune.
Test first and outside in
[00:19:27] Test-first development. I'll do this quickly, because I want to do the last slide in a bit more detail. It basically just means that we do what we need to do, and we stop gold plating and adding stuff and going, "Oh, we need all this shiny stuff in case they ask for it one day," when what they ask for is something completely different and we have to find it and take it all out again. Maybe it feels like it's going to take us longer. It's a way of being confident that what we're writing, from the very first line of code that we write, is exactly what's required. And that's a good feeling.
[00:19:59] It's all about understanding what you're doing and why. No one wants to be a mindless drone. Software engineers are smart people. We employed them because they're smart people, and therefore we want them to feel that they are contributing their best selves to whatever product they work on. So let them understand why. And if everyone understands why, then what we come out with is something we can all be proud of.
[00:20:21] Acceptance criteria are absolutely key. They're defined by the development team with the product owner, shared things; we're going to cover ATDD in a second. They're business readable. Gherkin is a popular format for this; given-when-then is part of that syntax.
[00:20:43] Think from an outside-in perspective. Classic TDD goes: I'm going to start at the very smallest class level and work my way out. Fine. But a better way to do it is outside in. I am a consumer of this system. If I go and do this thing, then what are the other things, the collaborators, that need to be in place behind that to make it happen? That means that we're thinking about the customer journey, not how we implement it technically, and that's very key.
[00:21:14] Expectations are behavior. We talk about what we want the behavior to be, not the implementation. We should in theory be able to change the entire implementation, rewrite it in a completely different language, and all of our functional tests should still pass, because nothing should have changed in terms of behavior, regardless of how we implemented it.
The ATDD cycle: discuss, distill, develop, demo
[00:21:38] The ATDD cycle: discuss, distill, develop, demo. We had red, green, refactor in TDD; we now have four. When we discuss, the team starts asking questions. You come to the team and say, "I want this thing," and they go, "Okay, how will that work? What would it look like? And what about this other thing that you asked us to do? And what about this technical hitch that's going to stop us from doing that at all?" And they might, from that, add new stuff to the backlog. The team, the product owner, whoever it might be, might say, "Okay, we'll actually do some other stuff," and add that to the backlog. But they should at this point establish a shared understanding of what they want to do.
[00:22:23] Then distill. The team, again extended or otherwise, formalizes that acceptance criteria. They use something like given-when-then. We talk about logical functions; we say this is what we want to happen, the behavior that we want to see, based on the input we give and an action we take. They're using ubiquitous language, a language that both business and technical members of the team understand, the same words, and they don't include implementation details. At this point we still haven't written any code. We haven't even written any functional tests in actual code. We're just saying: if we do this, given this input, then we should see this other thing.
[00:23:04] At this point we're ready to go. We should all know what we're going to do. So when we take it into a sprint, it means we can just crack on, we can develop it. We don't have to stop because we don't know enough about it, wait for John or Taylor to come back from holiday so we can ask, and it just sits there on the board, when we could be spending that time developing other features.
[00:23:25] Then they develop. They develop that into automated tests using, as we said, exactly the same format. If we can do that, then it means that there's no ambiguity about what we're testing versus what we've been asked to do and are implementing. We can use things like domain objects. We can say "Jenny the customer," and we know that's a persona, agreed; it's in our domain dictionary. We can then go and instantiate that customer, and we know that it has all of these attributes. As a business, usually you just want to know that it's a customer: she's bought this stuff, she's a student, she gets a 10% discount, whatever it might be. Same words, but we can use them in different ways.
[00:24:07] We can add some technical scenarios. It might be requests per second, it might be the utilization of the CPU, whatever it might be, the non-functional stuff. And now you can make executable requirements, literally executable. You can hit run and those requirements are executed on your code base, and you can do it over and over and over again. When they come up green, you know it's doing what you want it to do.
[00:24:33] And then you demo. It might be that at this point you already know it meets the requirements, because those tests agree, and those tests are exactly the same thing you were asked for, in the same words. You might do some exploratory testing, maybe handing it over to another team to see if they can break it. And everyone goes, "All right, let's try SQL injection, let's pass in two million, whatever, strings of two million characters," whatever. That's what engineers enjoy. It's good fun. It's a high-trust environment; you can handle it. It also means they go and break it, and when they break it, we all have a laugh about it. That's the high-trust environment. And then you do the sprint demo and show your stakeholders: this is what you asked for, it does what you wanted, we just want to show you.
[00:25:26] The TDD cycle sits in the middle of that. Automated acceptance test red, then into the actual code and TDD, and then they're green on the way out. Everything inside is white-box testing: we know what the code is doing, we understand the code. Everything outside is black-box: we're testing it as a consumer.
The big picture
[00:25:50] On to the big picture, in my last minute. This is the longest and most ambitious slide I've ever created: a hundred and thirteen different animations. It's going to be wasted. Hopefully you're all familiar with this. This is basic product management, product initiation type of stuff. You have an idea, you prototype it, you test it, you have a feedback cycle, you improve your prototype, and then at some point you say, "Okay, we want to go ahead and do this." We put it in the development backlog.
[00:26:28] That backlog then goes through the process that we just talked about, and we start to build it. We build it, we deploy it, we measure it. We do things like analytics, maybe MVT, maybe A/B testing, that kind of stuff. From those things we iterate over it. We say, okay, we can probably improve this in a very small way, and then measure again. So it goes back into the backlog, we build it, we measure it, and so on and so forth. We also learn from our iterations, and customer feedback, and our beta testers, whoever those people might be. From there we feed it back into "what if we did this other thing, that'd be really cool," and so it goes on.
[00:27:12] That goes from qualitative over to quantitative, which looks like this. You have a rough idea; a non-functional prototype, something like a wireframe, whatever it might be: what if it looks like this? Then a functional prototype. Ideally this is where you start to get your development team involved. You've started to come over onto this side and say, "Hey guys, get involved. What can we do here? This looks really cool." And they're like, "Yeah, this is brilliant, but you've forgotten this other thing; that's a major technical problem, we can't do it."
[00:27:53] Then you might do some A/B or MVT testing and gauge the response from your actual customers or beta testers. You might do a selected territory rollout. We certainly do that: we roll out to our smaller regions and then we go out to bigger ones, if it doesn't all go wrong. And then our ATDD fits in here, between our backlog and build, largely, and this cycle happens between them, and within that is our TDD cycle.
