Overview

About this video

What You'll Learn

  1. Build a Cucumber Rust acceptance test suite for a git sync bootstrap feature in Given/When/Then flow.
  2. Map feature-file steps to reusable step definitions using cucumber expressions, regex captures, and quoted parameters.
  3. Set up per-scenario Cucumber world state with temporary directories, then verify repository bootstrap and clone behavior.

Ciaran McNulty joins David to wire acceptance tests for a Git sync tool in Rust with the Cucumber Rust crate, mapping Gherkin steps to step definitions, matching parameters with cucumber expressions and regex, then driving the Given/When/Then loop until the scenarios pass.

Chapters

Jump to a chapter

  1. 0:00 Holding Screen
  2. 0:45 Introductions
  3. 0:47 Introduction & Guest
  4. 1:10 Motivation for BDD
  5. 2:00 Background / Context
  6. 2:27 Recap of Previous Sessions
  7. 4:22 Starting Test Automation (Acceptance Tests)
  8. 6:00 Acceptance vs. Unit Tests & Project Setup
  9. 8:04 Reviewing the Feature File
  10. 8:20 Reviewing Our Feature File
  11. 11:20 Implementing the First Step Definition
  12. 12:26 Implementing 'Given I have no directory' Step
  13. 15:30 Writing Acceptance Tests with Cucumber Rust
  14. 20:22 Matching Steps with Regex & Parameter Capture
  15. 32:10 Implementing the Step Logic (Check Directory Does Not Exist)
  16. 41:37 Asserting Directory Does Not Exist & Verifying Failure
  17. 51:21 Implementing the 'When I sync' Step
  18. 52:59 Implementing 'When I sync' Logic & Handling State
  19. 1:08:40 Debugging the 'When' Step Logic
  20. 1:11:01 Implementing the 'Then repository is cloned' Step
  21. 1:22:44 The 'And bootstrapping completes' Step
  22. 1:25:17 Reflection and Conclusion
Transcript

Full transcript

Generated from the English captions. Timestamps jump the player to that moment.

Read the full transcript

0:47 Introduction & Guest

0:47 Hello and welcome to today's episode of Rawkode live. Today we're gonna be taking a look at behavior driven development test automation and to do that I have my favorite friend and BDD all around Guduk here in McNulty joining me. Hello. Hi, David. I'm glad to hear I'm your favorite friend. Favorite friend and BDD consultant. That was a Oh, okay. Strong end. Of course, I love spending time with you. I learned so much especially around this space which is one of those things I really wish I was I wish I could do it more often but again as a developer

1:10 Motivation for BDD

1:23 advocate, I'm not really a product driven person. And my role is probably just to hack on stuff. So tough. Yeah. I feel I feel the same way really. I do a lot of help teams do this. But then I leave again. Then don't know about you. My career has been a a sort of flip flop between working on a product long term, doing lots of different things. And you get bored of one, don't you, and want to do the other. Yeah. I definitely like to experiment and I'm always playing with new things. I was lately

1:57 like I'm a technology magpie. So it's not often that I'm just working on a product full time. In fact, I haven't done in at least three maybe four years now but now that I'm starting to find a lot more use cases for like, I've got a couple of open source projects that I'm trying to get to a point where they're usable by other people and I'm starting to think right. They're not products that I'm selling but there's still something that expose an API and expose behaviors that I wanna be able to make sure people understand

2:00 Background / Context

2:24 not just implicitly but through explicit documentation and so far in a series we've we've taken this get synced project which is supposed to be a project that allows people to build their own get up style operators and controllers that monitor the status of a get repository and then have certain behaviors act on that and trying to expose that to people is what have we done now about three episodes this is maybe number four. This is three, I think. The three. Alright. Okay. So we we did First time we talked about what you're gonna build. Yep. They mapped naturally actually to these these

2:27 Recap of Previous Sessions

2:59 different steps of sort of BDD process. And the first one, talked about what it was you were gonna build, and we did some example mapping as a way of having that conversation. Yeah. Which is what you do in real life. You talk to the you talk about what you're gonna build. Ideally, you know, early on before it's all been nailed down, you actually have as a group, decide what you're gonna build. The second session, we then we had a bit a gap, which is good because it's kind of what happens in real life. It's not the ideal.

3:28 We had a gap, and then we had to try and remember what the conversation was and write it down. So that's what we did last session. We wrote down in this Gherkin syntax. And, hopefully, both of those steps are useful on their own, but then this last step is we're gonna use that written notes from our conversation to drive our tests. And it's been weeks. Well, we kept the Months. The December break and, you know, we've eased into I've definitely eased into January. I haven't done too much streaming in the first couple of weeks. I've just been, you know,

4:03 slowly but surely getting back into my groove now. So it has been a while. And I think tends to encourage people to do these things over a short time scale but this time scale of like four to six weeks is probably realistic for a lot of people, isn't it? So we might find we don't quite remember what we talked about and we have to rely on our notes. Well, it's a good thing we have all these scenarios now inside of a repository that we can Exactly. To build together this test test automation. The fun thing is it's I also haven't

4:22 Starting Test Automation (Acceptance Tests)

4:32 written any rust in a good month, maybe longer now. So I'm hoping that all comes flooding back to me. Don't think you've ever written any rust. Is that right? Correct. Correct. Cool. So we'll use this as a bidirectional flow of information. So we have a hello. Hello, Mohammed. Thanks for joining us. Oh, hi, Mohammed. We we also have should point out this is something you've kind of already written. I don't know if the code you've got covers the scenarios we're gonna talk about. But we can come in and kind of pretend you haven't written it yet.

5:09 Of course. Yeah. I did start this, but I think what context is important, isn't it? What happened is I started hacking away on the code internally on a project and it worked. And then I thought, well actually I probably wanna use this on more than one controllers. I'm gonna extract it to a library. I extracted it to a library really crudely and then went I can't really expect other people to use this if I don't tell them how it works and I don't have testing around it and the nature of the library itself is all integration. It works directly

5:42 with lib kit like I'm not actually doing anything on my own logic, it's all just orchestrating this on the library. So BDD seems like a good way to write these kind of integration tests and ensure that it worked. Don't suspect there are any unit tests really for this application but maybe I'll surprise. Yes. So the kind of tests that naturally come from BDD scenarios are acceptance tests. So they're generally the black box at some level. We can talk a bit more about what level that block operates at, but you're kind of probing the system at some level and saying it

6:00 Acceptance vs. Unit Tests & Project Setup

6:22 should do what I want in this situation. You don't you don't really care how it's working beneath the level you're testing at. In some domains, you would then also want to write unit tests and integration tests and contract tests. But they're all kind of in internal quality metrics almost, aren't they? Whereas the if you're writing acceptance tests, you're kind of validating the external quality. Does it do the thing that it's required to do at the level I'm testing it? Which which are two different things. One is one is the acceptance test level, the external quality is

7:02 something that's on the on the boundary of where the business engages with technology in that both parties care about the external behavior, both parties care that it does what it's supposed to do, and both parties can sort of evaluate whether it does what it's supposed to do. And the internal quality is the professional responsibility of developers, isn't it? And it's and it's it's the developers decide whether they write unit tests, really. You can have a manager bashing with a code coverage chart, it doesn't mean they're any good. So all that internal stuff is kind of

7:38 away from the business, sort of internal to the technology team. Did we write any unit tests? Do we need to? Probably not in this case. Well, let's see. Shall we? We'll see. Yeah. Interesting to spot some tests that don't spot some worries we have that aren't covered by the tests we're about to write. Yes. Definitely. Okay. Let me let's let's fill it open. So here's our scenario first. The the feature file. Yes. We will. Yeah. Let's jump into the feature files. So this is all on getthat.com/Rawkode/getsync. I already have it cloned locally. Got some code open, so we'll float that away and

8:20 Reviewing Our Feature File

8:21 we're gonna start with the first feature. Which we did two episodes ago or last episode even, sorry. So I'm loving this not implemented annotations because well, we haven't really hooked any of this up yet. But I was really happy with the way that we built these that with this feature fail. It really came together, I feel. And we really destroyed what we wanted to happen even with just like the first basic use case. Yeah. So should we just Yeah. Go through this like example by example to understand what the application does? Yeah. If if you that's. Yeah. Is that

8:59 what you would do? We should be recognizing them. Right? So that what did you write? Git git sync expects to clone it. Oh, it's about cloning when it starts up. I remember. Yeah. We focused on the first feature was like when I run getSync as a controller Yeah. The first thing I have to do is well, it's gotta be configured in some way and I think we've intentionally omitted all of the config language because we hadn't really implemented that in the code yet. But we said that we would have a director that we need to get a local copy of and

9:30 there are some conditions and some edge cases where that may fail or we may have to do some magic handling. Yeah. And that config stuff. Yes. It's really important when you're writing tests or just with BDD hat on, right? Writing tests and describing something to a human is the same thing. So it's really important when you're explaining things or writing tests is to come up with a simple example, and there's a there's a sort of skill to that of picking things apart. So I remember when we were talking last time, we just started mentioning these config concerns. We made a deliberate

10:03 decision. We're gonna remove that from the conversation. So there's nothing in here explaining why it goes into a particular folder on your disk, for instance. We would expect though that when we build the configs, if think about it in terms of bootstrapping a project from the start, we might then hard code everything. Hard code all the config and get this working. And then when we want to build the thing where it reads a I don't know. TOML file? What do Rust people use? YAML? No. It's it's TOML. Yeah. That's when we'd write maybe a different

10:38 feature. When the file looks like this, then it should go into this folder. You know, all those edge cases. And we talk it through. So you you kinda wanna split these things apart in a way that they're understandable to people, but also with the sort of production thinking. You wanna split them apart in a way where you could deliver them separately. You know, we could we could build this thing we're looking at right now without worrying about config, just hard code stuff. Mhmm. And kinda come back to the config later. Good. Because I think that's how the code

11:13 actually works as well. Now, I wanna make sure I understand this correctly. Right? So when so when it comes to implementing the test, right, for each of these, do we just pick the first example and we start adding the step definitions for this to make it work? Yeah. Why not? Okay. I mean, really it's the order that you want to build it in. Think it's I'm just a little confused because I know that already some of this code exists. Yeah. It's tricky, isn't it? So sometimes you want this if it's something where that you understand how it's gonna work Yeah.

11:20 Implementing the First Step Definition

11:59 Sometimes you just work through it sequentially or you pick the happy path first. Sometimes if it's like if you don't quite know how it's gonna work, you might be able to pick one of the sort of edge cases that's gonna be easy to implement first just to sort of get you going. I must admit, I tend to just do it top to bottom if I if I feel like I know what's gonna happen because it feels like a nice logical way of progressing. Yeah. Because I feel that when we did when we wrote the scenarios that way as

12:26 Implementing 'Given I have no directory' Step

12:30 well, we we tried to keep the simpler things at the top. So it feels to me that if we start to build it as simpler test cases, we're starting to understand how the library is actually gonna work. And hopefully, of those steps become really reusable as we get further down the more complex stages. Yeah. When when we were the order we put them in the document is really for a reader. But that might naturally map to the order we do them in. What I meant about picking edge cases is sometimes it's like, I've got no idea how

12:58 I'm gonna do the actual functionality, but if I make myself implement the scenario where it throws an error message, at least it will get me typing something, and I'll have to name some objects. Objects and I'll have started thinking bit more and hopefully by the time I finished that, I'd have had more thoughts about the the core. And we discussed them with this two this so should we just pick the first example? Yeah. I think what I wanna do first is make sure so I didn't stall. I hopefully have for whom I need. I just

13:29 wanna make sure I run cargo test. I guess I should see all of the scenarios listed as not implemented. This would be They should not appear. They shouldn't appear. I'll expect them to be filtered out. Oh, okay. So we get the not implemented yet sketch, not implemented yet sketch, not implemented yet sketch, so forth. I'm I'm good. I like that list. I can see how far we are through as we build out. Okay. So let's talk about two different things. That's not related to the tag not implemented. That's telling you there's no matching step in

14:12 your cucumber file. Okay. What would normally happen is you'd run is this inside a test runner or something or make file or somewhere? So you'd you'd run Cucumber with and filter out that tag explicitly if you didn't want this. There's a difference between it means they're not they're not implemented that you're seeing. It's saying the test automation hasn't been implemented for that step. Whereas what our tag means is this feature hasn't been implemented in the code. Right. Does that make sense? Kinda. See a I can't remember what the flag is. It's like tags equals Well, I don't know if it's library specifically.

15:05 Maybe it's The cucumber's the cucumber CLIs tend to have very consistent command line flags. Are you running QCamera at the command line? Yes. Through this Rust library. Yes. So I'm using cargo's test tool, which then uses a whole bunch of macros to generate Rust tests and all this crazy weirdness. But I don't think it's important right now is it? That's we should we should be able to know just to say, hey, okay. We haven't implemented this scenario. So let's let's start and see what happens. Yeah. Okay. So let's pop open our test suite here. So if people have never used Cucumber, what's

15:30 Writing Acceptance Tests with Cucumber Rust

15:49 gonna happen is Cucumber is gonna run through the steps in the Gherkin file and for each given when or then step, it's gonna try and execute a line of code. So all and that's all QCamper's doing, is it's matching these steps to lines of code that that David has to write. So we have to tell it what to do when it sees, given I have no directory called. Right. Let's see. Is it still legible if I just do a split here? Not quite is it? Alright. I'll just copy this first scenario into my my code I think so.

16:30 Okay. And we'll put it here. Hopefully we can keep that kind of and display as we start. Now I am not overly familiar with this library, I have a month ago I attempted to try and stitch this together and then we talked about doing this episode and I thought okay, let's let's wait because we'll just do it together. But we have this builder thing where I can say I wanna handle a given step and the name I think just matches what we have and say of our scenario and then we just we just tell it a function to run to execute

17:11 that step. So Okay. That means I can do given I have no directory called get stink. I'll need to work out how to parameterize that. Then we run some function and this function I've already got. Yeah. A couple of them here as examples that I tried to do previously. So we want to do nothing. So I have no directory called get sync. The implementation there would be doing that. Yes. And it's not because we changed the step. Right? I have to actually enforce that that directory does not exist. Yeah. You could just check the directory doesn't

18:02 exist or you maybe would delete it. You gotta think about, like, if your test crashed mid run and things like that. So different ways of handling it. What I'll give you some options. Option one is we check there isn't a directory called git sync and we throw an error if there is. Okay. And the operator has to fix it. Option two, we always create a new temp folder and use that as the current working directory in our sort of setup. So that kinda guarantees there's never gonna be a Git sync folder. So we're always making some, like,

18:40 unique ID prefix temp folder. Or we do nothing. We hope the next step crashes. Okay. So I do the second one there that you mentioned. So when this is the setup method for the world, the world is just the state that all of these scenarios run-in. One of the things I do as part of the setup is create a temporary directory. Okay. So this should be available to us and side of our do nothing. So we should have a clean environment. We can access world.dir if we want. However, for the moment why don't we just

19:31 do let's just do a print right and see we're in just I just wanna see it running although I think the printer actually swallowed by the test runner. I am a new world and directory and we'll just tell it which directory this is. The world that there. Like so. Okay. Yep. Let's do that. Alright. Okay. That does something end of the line twelve eleven. Sorry? Do you need a semicolon at the end of line 11, I think. Oh, yeah. I need to fix that. Yeah. Why is this complaining? So okay. So I need to fix it. She's

20:22 Matching Steps with Regex & Parameter Capture

20:22 one of these annoying rust things where I have to wrangle it into something that can actually be displayed. So does a path have display? No. So just to explain for the watchers. What you're doing in your in your this is called a step definition file. What you do here is you define for each line of text what code should be executed. But you're not doing it in any particular order. You're just defining a kind of dictionary or a glossary for Cucumber to use because the same steps might happen time after time in the Gherkin. We have one one function here that's gonna

21:03 get executed. And so the point of the world object, each scenario, each example is independent. But when you run each example, you want it to have some sort of shared state. So maybe we create something and then a later step references it. The world is where all that state lives. So it's just somewhere for stuff to live. And it gets destroyed at the end of each example and created new for the next example. Or it should be. I haven't used Cucumber Rust. Okay. That should maybe work. Yeah. It seems to have to work. Okay. Let me try and explain this as best

21:41 as I can with my limited knowledge. So we've hooked up the given. This is actually not gonna match because of this. So we need to look up how to do that. And then tell them that the test function is do nothing. We have or do nothing here. Now the way the print macro works is that when I pass in this, it actually looks for a trait on the the structure that knows how to display the value, which this does not have although never yeah, there we go. So we can force it into debug display with that which just means it doesn't

22:13 matter if it doesn't have display just print out the bytes or whatever we have access to. So I'm just trying to print it out. Now let's confirm my thing here that this won't work. This won't match. So we should just see all the skits. Excellent. Not excellent but now we can work out what we wanna do there. Did it skip it? Hang on. Let me go. We have because we don't have a match yet. That should be alright. No. Because we have this quote called get sync and quotes and here I use different quotes. I think just escape them.

22:55 Could but I was gonna try and parameterize it properly. Let's let's see see if this works first. Okay. Okay. Okay. Let's try. I just forget how long things take in compiled languages. K. Alright. So it's not matching. Okay. It could be there's magic syntax here that understands when that's in quotes. There's some sort of variable capture. Could be so So why don't I take that out first? Let's open that feature fail. Yeah. Just put like, given I have no directory. Oh, yeah. There you go. And then we'll work out the last bit. So that should match exactly.

23:53 Why is there a hat at the start of that? I shouldn't have the Kevin. I think that's why it didn't match last time with the escape. Okay. Why is there a what? Sorry? I thought there was a start of expression at this start, but maybe it's just because I'm looking at it on the wrong screen. There we go. Did you have a pair here? No. There will be a way to turn that on. We're meaning that we'll we'll work it out. I have run into problems with the rusting swallowing them before. I know there's an environment

24:42 variable to disable it. I'll grab it from another project and which I'm pretty sure I have that in a make fail. But right now, I don't think it's important. Okay. So yeah. You you want to add a pattern matching thing there, don't you? Yeah. So let's see how we do that with this library. There is There's a thing called cucumber expressions but it might not support it. Let's see. Language but oh, it looks like it does rejects. Well, have their own test written and this of course. So You can do given underscore rejects. Okay. So there is an everything is great for

25:26 wrapped in quotes. So yeah. I knew that triggered some sort of capture. So how do they implement that in code? So capture runner. We're sort of given. Oh, this is like a completely different syntax. There we go. That is completely different. I was hoping for just a dot given. You look in the in the documentation, it says use given underscore regex, and then you can make it a regex, and you can use a pattern. Sorry. The other there's some then rejects and things like that. You can see how they do it. Right. Where's their docs? Where's their docs? Here.

26:24 On this page, scroll down. See the then underscore rejects. Stop. Stop. Oh, yeah. Yeah. Yeah. Gotcha. Gotcha. Gotcha. If do that, you'll able use a regex as your pattern, then you capture the matches. But I I imagine it'll be given rejects. Okay. Yes. We have given a sync, given rejects, given rejects async. I just want rejects. Okay. Yeah. I don't know about async. So now we can pass an a rejects literal and I'm pretty sure I can use anything I want here because we need access to double quotes. So let's do that. I might be wrong about that. I'll come

27:11 back to Yeah, I'm not sure anymore. We expect it to be I have no blah blah blah called blah. Yeah, it doesn't like that, does it? Okay. Rust rejects literal. I am a Google driven developer. Do not judge me. They're just we can just use the quotes and quote them. Duck duck go. You always just get rubbish or codey stuff. Why is it not just returning the language construct for this? Never mind. Let's do it their way. I'll come back to that and I'll escape for now. So this is now a captured group. And what we're gonna see is

28:28 alphanumeric. No. That's alphanumeric. Yes. Spaces. That's not that's spaces. That's not space. Do you put a dollar at the end for safety? For safety. And this changes our function definition now where we actually need to pass in. I know I could probably still pass it and like, so I just need to make sure and I should probably rename this. This is directory does not exist. Mhmm. And we know Yeah. Sometimes name them actively, like check directory doesn't exist or, you know, it doesn't matter. K. And at least one I want us to know what matches is.

29:23 They don't use an external function definition. So Excuse me. Tell me the type. There we go. The list of strings. Yep. See. Not a single. It's better to arguments phone one. Look, your string is finished there, is it? At least the highlighter thinks your string is So it takes it out. Itself, and then it takes the name string, and then it takes the test function. There's something funny with the created string. The highlighting seems to be indicating a problem. Yeah. Okay. So Oh, no. Still a problem. So we have a mismatch types here as a vector string.

30:45 Okay. So what we expect is is a back string. K. That keeps that happy. Now we wanna get these quotes in. Yeah. Yeah. It's not happy with that. It's a reject string. What how do you No. I was this is just a raw string literal. Hold on. I'm gonna I looked up the wrong thing. So it's raw string. REST raw string literals. There we go. I'm pretty sure you can use anything. Yeah. There we go. So I got the syntax a little wrong. So we do pound and then I think it can be anything I want and then I can end

31:41 up with that and now I should be able to just put that in. I'm making it really angry at the moment. Hold on. And then we end up with the same way with the hash on the end. Doesn't look right. Right. Okay. We got there. It's happy. Right? It doesn't look right. It's right. So what I'm gonna do here is print the capture and our configured environment. So we are saying I'm in a new world and directory and don't expect this parameter which we should be able to do as matches. And this is a vector.

32:10 Implementing the Step Logic (Check Directory Does Not Exist)

32:41 I guess we can just print out the whole vector like so. And now we need the rust format log thingy which I'm sure I've got in this directory. Rust FMT log tests. Just go to Google. Rust show prints and tests. There's like a weird environment variable. That's quite old. Let's send a new article. Mhmm. Rust log. And no capture. Okay. Let's hope for the best. It may work. Okay. Let's see. So this is print output. Okay. So this is using the ENV logger from my actual source code which I'm not using in my test cases.

34:17 So let's use that instead. So it's it's also not matching the step. Oh. Let's test my rejects then. I'm sure I probably just messed that up. So I have no directory called get sync. This is our string that we're testing with and it worked. Let's make sure I didn't change anything in the feature file. There we go. Alright. So in some languages, this is slightly easier because your IDE will know about Cucumber. I'm gonna let you click from the step into the definition and tell you when they're not matched. Yeah. That step's passing. So New York Cucumbers have this paradigm where as

35:43 long as there's no error during the execution, even if there's no lines of code in the step definition, it passes. Looks okay. So Well, we need to what we should do is make it check there isn't a folder called git sync. Okay. So instead of jotting. First, check if directory exists. I don't remember how to code. Yeah. I'm trying to share before I even look at that. I'm pretty sure it's gonna be really trivial and it's gonna be like, we just want path equals path or from whatever we want it from and then there's You kinda want it to be the current

36:39 working directory, don't we? Path new exists. That was pretty close. Okay. So So can you assert that or something like that? How do errors work in Rust? We could do an assert. Yeah. So if we do an assert equals where we've got we don't wanna direct you to exist. We wanna use the path module. We can just do from I know they use new but it doesn't really matter. And what we're passing and is going to be the matches. Give me some help here. That is the zero dot exist. Like so. Run it. Yeah. That's vector thing. I'm trying to remember.

37:42 It's better destructive path. Than struct string. Okay. Rust vector pop. I don't wanna peak to show my head to the pop. V pop. I really should just trust my instincts now and again. But anyway. And we want this to be wanna see what we get because it's a vector string. There we go. No, we don't. It depends if the regex library passes you the full phrase and the captured group as well. It might be is it one or two? Zero that you want? Alright. Let's get this working then. Do have a debugger? I'm turning you to rust to be able

38:50 to whip out a debugger and use it effectively, I'm afraid. I mean, I'm turning you to even be doing what I'm doing now but I'm gonna keep sticking with it. Oh, I actually have a check of exists unit tests but let's ignore that for now. Yeah. I mean, you can mix on that. I mean, you might delete some of the unit tests after. It depends, though. Often, you'll have written more unit tests than were in the acceptance test. Like, you might be able to think of more edge cases. There might be a example of the directory

39:24 being there but not writable, for instance, in the acceptance tests. And then you, the developer, might be able to think of eight reasons why a thing isn't writable. And so you'd maybe do those eight things at unit test level. Alright. So I'm just gonna enable this end logger. Wonder if that would just work there. There is a way to get print output on a test and now I need it. Rust test println. Yeah. No catcher's not working. Thanks. It didn't work, did it? Let's try one more time. Although I don't see the output here. The

40:43 past. I know but I didn't see the print statement. We're beyond prints now, aren't we? Take the print out. I've looked in the RS lane 26. So I'm using info here. Okay. So it's already enabled in the project. I think there's something where you can Now you've already got harness false. I know I should turn that on. Okay. Call So take out the print take out the print anyway. Let me change it to debug. So Oh, and I put that in the wrong place. So test live in our own module and rust. So I need that here.

41:37 Asserting Directory Does Not Exist & Verifying Failure

41:48 Push enables that macro. Okay. And then we run this and I want to enable debug logging. It's coming back to me. It's a little bit surely. No. I don't see any output there. Do you? No. It's matching there. Rust log debug. I use debug. Alright. Let's just get this start working. I wanted to kinda know what matches was. Yeah. Alright. Let's just try and work through the error message. So it's complaining that I maybe popped isn't guaranteed to work at compile time. So I don't think that's a problem. Let's do that first. That should satisfy it. Now

43:03 what I wanna see is that we have some path which we know to be a string is I don't know why it keeps messing with my code. Gonna come from matches which is a vector or string We want the first one Mhmm. Which returns an option and I wanna be able to unwrap it. Now I should be able to use this here. Maybe it's because we didn't unwrap it. Still not very happy about it. Oh, you know I'm gonna have close the records. What is excess return? You want another closing bracket at the end. No. It's not always tight.

44:06 So what is excess return? Oh, no. I'm doing this wrong. From path dot exists. Close. There we go. User error. Okay. Now we're getting the size thing again. Let's get playing. So what happens what happens if Let's just unwrap this. What happens if matches doesn't have any elements in? What does unwrap what's unwrap then? Okay. So we could well, it should add her, but we can also do Okay. No. We just want yeah. Let's just let the add her. You type for me. I think if I do it as a reference, should be I wish I was better at this.

45:06 I definitely have a string here. The problem is is that path dot let's see what path dot from expects. So it supports multiple types. What about if I just use new? That one's an OS string as a reference. Okay. We can make that work. So new. Is this what I decided not to do earlier? Yes. And I'll pass in a ref. Why are you complaining now? Why is it error message that was at the bottom? And the one set as an OS string. Why do you hit me so much? I'll show a path buff and I'm I'm

46:23 winging it now. So that's my string. There's my path buff. Please just be happy for once. I haven't used it. Okay. From. Oh, no. Why is that unhappy? Oh, yeah. It's immutable. Guess we can fix that. So as we can do a colon. Is it finally happy? It's Wow. That was painful. Mhmm. Let's see if our assertion works. It still works. Okay. Do we trust it or do we wanna create a directory and see it fail? Well, so what I would do is just change the string. So never trust the test that is broken broken broken passing.

47:56 You wanna see it fail as well. So can we give it a path that's clearly got a file in? Well, yeah. I'm assuming this is maybe a root path as well and we're not actually using that temp directory that we put in. So let's fix that. We can confirm that with a we can confirm that. That's all I mean. Yeah. Okay. Let's create a directory. So just show me. Okay. Creator. Thank you. Alright. So let's cause this to actually break. So we want path from world doctor, which is our temp directory joined with our matches.

48:54 Oh, no. We have already got that here. So pop that up. I don't know the directory is terrible but we'll call it path just now. So that should create it once it's happy and port fs. Rest. And whatever. I don't know where that goes there. Oh yeah, I can't use the question mark because we don't return a result in this function. Okay. And now path is complaining. Let's go back to path buff. Okay. It seems alright. No. This is complaining down here because this has moved path. So Rust does this thing where every time you pass something

49:59 along, it moves it in memory so we can just say pass a reference to it that keeps that happy. Why is world complaining here? Why is world? Oh yeah. So much fun. Okay. So now this creates a directory which is a directory we expect to work. Let's copy and paste this exact code for that path and this test should fail. Why is it going red? Just because I hadn't finished. Oh that's actually that's just complaining because I'm not doing everything with the return type. I miss a yellow squiggle not red. Now let's run that again. It should

50:43 should fail. Right? Yeah. You'd hope to. Did it? Yeah. True false. So now we know this code works. If we take out the create. Yep. Then we should be in a passing situation every time. Yep. There we go. We have implemented one step in fifty two minutes. Yeah. I mean, of that was our fault. It's worth mentioning. If you had other examples that also said, given I have no direct recalled something, then that they'd all be passing as well. So you can see all the other scenarios have one saying, given I have a directory called git sync

51:21 Implementing the 'When I sync' Step

51:40 or a local claim. So as soon as we implement each one of these, they it it has knock on effects for the other scenarios, the other examples as well. So what's the second line? What can we go back to the output again for a sec? Yeah. So now it's complaining when I sync the blah blah blah blah blah repository. Mhmm. It doesn't it doesn't know what to do. No. Because we're so familiar with this one step that we have spent so much time and love and energy on. Would you not then copy and paste that

52:13 to satisfy pretty much every other well, it would just be this one, wouldn't it? They all look different to me. Was I have a direction. Yeah. That's what I mean. Would you just would I just copy and paste this and implement that given step or would you just continue with the one example? You could do. I would I tend to just get one example working because I kind of think at the end of the example, I'm gonna commit. And I'd probably be filtering out the other scenarios. Like, if you look at the gherkin. Yeah. The gherkin. Yeah.

52:49 In terms of like breaking big chunks of work down to little chunks, I'd quite like to get that first one so it works. Okay. Let's do it. So Without without any error handling. But, you know, in that happy in a happy case where there's no path already, get that working and then, you know, take the rest of the day off. Have a long lunch. Anyways. So we got a given rejects. Now we need a when rejects where a rejects here is gonna be something followed by I'm just gonna call the same function for now. Yeah. You've got it in common top online

52:59 Implementing 'When I sync' Logic & Handling State

53:35 18. Okay. So we use the same function just now so the compiler is happy. But I'm gonna modify the string and it is I think the something repository and this can be pretty much anything. I think we just need a pretty wide capture here, don't we? And then we end with this. I mean we could do okay. Allow Not not. We allow forward slashes. We allow colon. I I normally do not double quotes in square brackets. Hat. Carrot. Double quote. Escapes properly. Well, seems alright. Well, let's test it. So we've got tester. It's mostly escaping inside the Rust strings that I'm

54:43 concerned about. Okay. So we expect this to match. Gethub.com/Kieran/David. It doesn't we also want it to match a private one. No. We we want it to match. I sync the Yeah. Okay. It seems quite happy. Right. Now we need a new function. So this is f n sync get. I'm just gonna copy. Yep. Function signature. So the the world would be for it to pass. So in some places, like, I don't know if we've done it in this scenario, but you might in a later step say, on that directory, you know, it should be cloned into that

55:53 directory. So we might in that first step that we where we check the folder exists, we might want to stash into the world the name of the folder. Do you see what I mean? So even a later step where So it is in the world. You got The direct the temp directory is stashed in the world? No. The the name of the directory that we're cloning into. Let's tackle it later if we need to. Oh, okay. I understand. The world is somewhere where you might want to keep things for later. As a general rule, and, you know, very general, and you can

56:35 break it sometimes, you wanna be storing values in there. If you start storing entities or services in there, it gets messy. Okay. So let's do that. We do have a directory path buff there. Oh, no. We saw the temp in that. So really what we want is like a clone there. Just a string. Just a string. Name of the folder. We can defer this until we need it later. Might make more sense. Okay. Calendar by default will be blank. Yeah. Oh, yeah. It's a string. Bank. Okay. And then in the step we can populate it.

57:36 Okay. In the given step. I mean, we could try. So what we're seeing here is world dot cloned or is now equal to our path. Yeah. Yeah. So it means in a later step if we if the step doesn't have the name of the directory in it, we've got that context. If that's that in that's that state that just lives for the example. Why is it not letting me make that mutable? Oh. Oh, because when I pass it in here. There we go. Right. So we should have access to that here. So what we're saying is we actually want

58:31 to clone it to that location. So I'm just saying if we needed the string. Well, I mean we would anyway. But what's this step gonna do? This is actually gonna use our library, isn't it? Yeah. So we talked about this last time, but let's recap. We've if you look at the Gherkin again, we've got we wrote five or six examples. But we decided that if if they'd all been tested at the library, we wouldn't need to test them all through the CLI application. You still believe that? Yeah. Yeah. And we we even tagged them. Right? Can

59:09 you show that? Mhmm. We tagged if those first two are tested through the CLI, in the other examples, there's nothing that CLI is doing differently. It's just handling whatever error the library throws. So we're gonna for a first step, we're gonna test everything through the library. And then later, David can come and write a different set of steps that shells out to the CLI command instead, instead of calling things directly in Rust, which will be slightly slower as well. Okay. Yep. Okay. So that means that we need to get sync where we can figure the repository,

59:58 which comes from our let's just repo URL is actually equal to our matches.pop.unwrap. And then that now becomes repo URL. We also need to specify the data clone it too. It's repelling from the world. It's our clunder. We need an interval. So this is some of the configuration that we said we just weren't gonna tackle. Seconds. Call it 30. Username, no authentication, no private key and no passphrase. So that gives us a get sync object that's not doing anything yet and I can't remember exactly how this code works so let's take a look. Once we have the structure,

1:00:55 we can just call clone repository and it should not return an error. So that means we need to first assign it, So we'll call this get synced and then what we want to assert is that we do not get an error we can actually just call the function to do Yeah. So in given you often have to check something or do Well, less often in given, actually. In when steps, which is at the end, you often have to check something and then throw an error. Most of the time in given and when steps, you're just issuing commands

1:01:31 and you're trusting the underlying thing to throw errors or the when won't pass. And and this git sync object, Dave, we there's probably some way of initializing that ahead of the steps, isn't there? Either attaching it to the world or we do that later. There'll be some hook. The the world life cycle is is to the example, so there's probably some way of creating it in the world at the start, attaching it to the world if we were gonna be using it in lots of steps. Okay. Mhmm. Ugh. Why do you keep complaining at me?

1:02:19 I'm just gonna do this to hack you and then I'll come back and fix it properly. So cool in world world. And then I'll use my cool in world. Cool Yeah. Don't hate me. Well, it doesn't implement clone. Alright. That means I need to work this out. Okay. So the get sync library doesn't take a reference to a directory. It expects it to be mutable. I don't know why. That's probably just a bad design decision. So what I'm gonna do Well, never has to it should never have to modify on that point. The code that is in the step definitions

1:03:20 is very similar to the code that people using in the library are gonna write. Mhmm. So something that's benefit of test first is that, you know, we'd have written these lines without having implemented it yet if you're doing pure TDD. And that means that your application API is gonna be very aligned with the use cases. So the line we're matching at the moment is given us, you know, I sync the repository. And the fact that came from a conversation means that that's something we're thinking about as one operation. One, you know, one business action. So

1:04:01 that should be one API I call that's relatively simple and maybe only takes the parameter, you know, largely only takes the parameter that's mentioned in the text. Yeah. That's a really frustrating thing with Rust and you can't have partial structures without throwing lots of optional types around. Yeah. So you actually do need to specify everything here but I could definitely Yeah. I'll need to work on that. I think the design can be nicer. We shouldn't always have to specify this stuff. There is a default tree I could maybe apply to it that would allow those values

1:04:39 to come out as none without being explicitly specified. So and you also try and match the naming. So what was the text of the step? I've I've forgotten. Scroll up a little bit, and we'll see what the the git the gherkin text was. When I sync the repository. So you want the you ideally, you want the method to be called sync and the parameters to be called repository and things like that just to make sure you're really matching it to the you know, that DDD thing of ubiquitous language. Mhmm. Try and keep the objects named

1:05:16 the same way, that kind of thing. Yeah. So the sync is what happens after the clone when it enters the reconciliation loop. So I think the language in the feature is probably incorrect here rather than No. I think it's correct. When I sync it, then the first thing the tool does is it clones it. So I I the user think of it as a sync. I don't care about when it's initially cloned. Right? The way the the user facing API doesn't ask the user to clone it first and then sync. It's like, hey. You just start

1:05:46 syncing. No. It does ask I think it does ask it to oh, yeah. Maybe I could fix that. Yeah. No. I'm designing it from as if we had hadn't written the code yet. Yeah. The the kind of when we're in the room when we're in the room with those business guys that we were pretending in the room with, Every time they talked about this feature, they said, when I sync the repository, here's what should happen. And they didn't talk about a clone step, so we kinda captured that in the way we wrote the scenario. And therefore, we should try and capture that

1:06:16 in our API. Okay. If we can. Let's run this test first. Yeah. Don't rewrite it now. I'm just making points for the viewers. It's not a valid regular expression. So the escaping thing you were worried about has no. Let's see why. I think that quote that double quote needs more. Yeah. Maybe it doesn't need. Yeah. Either add one or remove one. I'm not sure. Let's see. Which version was that? Depends on when it compiled and when I saved it. Well, look, it matched. That's the important thing. There we go. Okay. So Yep. Worked. Now I think I don't trust it. Yep.

1:07:25 Agreed. So let's how can I make that function call fail? Let's take a look at it. It will fail if Actually, we maybe don't need to. I mean, we'll find out it's failed when we get to the when step, won't we? Well, we can copy that line of code where we create the directory and to this step and if clone should fail. So we can just do that. Confirm. What have I got wrong? Path is now world dot cloned there. So I don't think it'll clone into an existing directory. Oh, it did. Wonder because it's empty. It

1:08:29 doesn't care. Can you look on your disc and see what's going on? I could have I knew where it was calling to or not getting that output. I mean I have rest log enabled. Why is my debug statement not coming out here? Let's try header. Right. That should probably always print and then make sure I have the right import. Oops. That should really let that should print out the messages. I'm assuming the best BDD library is doing something on top of that to hide those messages. And that's not really important to us right now. So

1:08:40 Debugging the 'When' Step Logic

1:09:41 let's just create a fake fail rust touch fail. Maybe we should press on and go to the when step. And the when step will fail if it's not cloning properly. And the point of the when step is to check it's there. Alright. Hold on. You don't really need to add all this extra debug. The when step I just don't trust anything, dude. And it's you keep going. I'm saying it's for the viewers. The when step is gonna check whether it worked or not. So all this extra stuff is just for extra debug for future developers.

1:10:22 You know, you anything you add in your given and when that checks it worked okay It's just to make failures a bit less mysterious next time around. So if that failed touch works, then the clone has to fail surely. Have no idea what my code has done. Okay, let's carry on and see what happens. So the one step is oh no. Then the repository is cloned and the git sync directory. Okay. So then reject. Alright. Getting the hang of this. The repository is cloned and no. Do we need to specify this again? Do we really use it from the world?

1:11:01 Implementing the 'Then repository is cloned' Step

1:11:36 What would be the right approach? We have specified it again, so let's use it. So we we we write the things out in a way that is clearest to a reader. The the what I was saying earlier is if we'd written and the repository is cloned into that directory, we'd have been able to pull the the string, get sync out of the world. So so write it for a reader first. Try not to try to make things clear with what details are in which which line. Mhmm. Drop out details if there's too much stuff in there.

1:12:21 Okay. So this returns world. And this one is gonna be called do we Check directory exists. Check there is colon. Oh. Yeah. That's that's stronger, isn't it? That's our function. Looks so. So how do you check if directories of client of a repo? I have a method for this in my library. Great. Adds a slight point of failure, doesn't it? The library could just lie to you. Maybe I don't. I can call it a public I thought I had a test that opened the origin. Oh, no. I do here. Yeah. So does clone exist? Yep.

1:13:22 Makes sense. Hang on. What was that about not managed by git sync? So that's one of our other scenarios. Maybe we shouldn't use that function. So that's actually checks for the motes. So I guess we just want to copy this code and use it as a test condition rather than using this function as a No. Think let's call it. Right. Okay. So I think what we wanna do then is store to get sync and our world, which we do. Actually update it then and then just call that method. Mhmm. So if we have our get sync

1:14:03 here, what we actually wanna say is So that is there somewhere else we can set it? How do you mean? Because now we're relying on running this step before we run the latest step. There may be a way of initializing git sync into the world before every example, depending what kind of hooks system Cucumber Rust has. This will work. Okay. But we'll work copy and paste it for now. I think you're right. Yeah. There there is probably some sort of setup I can do around this. Because you just later on don't want to have a scenario where there's a different first

1:14:49 step and then it checks the folder exists and then it errors out because there's no git sync in the world. Okay. So I should be able to do get sync. Bet you that's not a public function. Am I making it public for this? Just for today? Let's see if it works. Right? And then We'll refract to remove it. Failed. It's remote does not match get synced. So So maybe we're doing too much in that. So let's just steal some of the code. Let's just steal some of the code. That actually it should be okay. Now hang on. The the repo,

1:16:21 shouldn't that be a URL? Go back up. Go go back to the step definition. What are we calling it with? In the check go down a bit. Does clone exist? But we're We're calling on this. Where's repo URL? Where's repo URL coming from? That's wrong. It comes from the the match. Oh, yeah. We don't have that here. That's not in the step. So we what we need what we need to have done is in the previous step where we did the clone, we need to have stashed the repo URL into the world. I think we No.

1:17:14 We don't. Okay. So let's add. And it gets a bit messy with having all this state, which is why it's a good idea to only have values in there. You can imagine how messy that would get if you had I say anyhow, values. I'd just encourage you to have the git sync service. But don't have entities in there. Don't have mutable objects in there. You wanna be storing IDs of stuff that you could then gonna retrieve again from the relevant services. So folder names, repository URLs. Missing one twenty. Okay. So by default, string from. We've got a partial move.

1:18:18 Thanks. Rest. Let's Now I need to make it mutable. We put URL has moved here. I need a clone of it. It's horrible code but I will fix it later when I understand it more. That's here as an as a mutable reference to it. Okay. Or not probably mutable reference, but as an owner reference and it's moved. But I'm just gonna clone it for the time being. Because that's my answer to everything. And then we have something similar here. Partial move of world. Okay. Just cool it. Whatever. Rest can be a little infuriating at times,

1:19:46 but I still enjoy learning at the same time. It passed. So we have a cloned repository. Now change the name of that folder in the gherkin and run it again. Okay. Here? Mhmm. Don't know what happened there. So that should fail. Yeah. Always always check it fails. No. Okay. Why? Well, because we're using Probably not using We used the clone there from the previous step. Which is okay. It's just you got something in two places kind of thing. Yeah. Let's just match it So I would do that. If it look if you thought it was

1:20:57 redundant in the gherkin, you could just replace it with the phrase like with the same that directory. And then it makes more sense that you're getting it from the world. It's more of an expectation. Yeah. But that that's not what we have noticed. I'm gonna do this. Fail, fail, fail. How many matches do we have in that lane? Have I done have I done something silly? No. Why why does that work? Why is it passing? Oh, we're not asserting the functions fine. Assert equals. Good. Good work. Yeah. So we expect that to say, yes, this clone exists.

1:22:19 It's gonna fail. So this is a good example why it's good to do that little sanity check. No. If we change it in future Now when you fix it now when you fix it, I'm gonna feel like it's actually testing something. And I'm gonna feel a lot more com confident here. There we go. Great. One scenario done. Five more to go. Let's go. Well, we haven't got and the bootstrapping completes. Ah. Yeah. Does what does what would we actually check? That's a tricky one, isn't it? Because it's like it hands off to the next stage of the process.

1:22:44 The 'And bootstrapping completes' Step

1:23:04 I don't think it means anything, to be honest. Does it? Maybe not. So you could take it out. You could I'm really having an empty set. Yeah. I think why don't we make it empty and then I think when we start to add the reconciliation stuff later then maybe we'll understand a bit more what it means but right now I think there is a handover and we should document it as such even if I don't know what that means yet. Yes. So maybe just a comment in there. So and or maybe there's no and maybe it's just

1:23:49 then again and the function would do nothing. We have a do nothing, don't we? I thought we did. I'll add it back. Yeah. Well, maybe it'd be good to document. World. Nothing. Close that. Button. Oh, yeah. Because that was a regex. We don't have a match and it do nothing. So we need to take that out. Should fix it. Not implemented. Oh, I've got the and and the master. There we go. So if we're doing TDD style, right, you'd have been writing the code as you went. So it would have taken longer, but would have been sort of interleaved into

1:25:17 Reflection and Conclusion

1:25:28 the time you took to write the code and think through the problems. Possibly, you'd have ended up with a slightly cleaner API or at least more use case focused, but you still have had to solve all those tricky problems that you undoubtedly had to solve. Uh-huh. I mean, I think you're right as we were putting that together, there were the ways in which the code is being called that I don't think I'm necessarily happy with. So I wanna clean up the API definitely. But I'm happy that we have one passing scenario and I see those steps properly from the basis of most

1:26:04 of the other steps and the other scenarios too. I'm hoping like there's not hopefully gonna be too much other weird stuff. In fact, yeah, looking at those other scenarios, they're all get clones but with a different origin name or a different remote like those would be relatively easy to to tweak for each step and reuse those step definitions. So that's quite exciting. I'm quite happy with that. Right now, it's it's quite short in the end of the the code. I think it's a good a good rule of thumb is that your step definition shouldn't be too long.

1:26:39 Because if they are, it means that you should probably write a method that does a lot of stuff. Because it's the kind of the code that goes in those steps, it's the kind of thing it's the kind of code that will go in your CLI command wrappers. When a web context, it's the kind of code that will go in your controllers. It could be like calling calling the real logic. So if if it's something you think of as one step when you're talking to people, you should make it, you know, an action. And if you're doing event sourcing or CQRS

1:27:10 type stuff, it should probably be a command, sync the repository. It shouldn't, you know, it shouldn't map onto one thing being said along with us. Yeah. Well, I I think I need to work out what happens after the bootstrapping completes, how the reconciliation loop starts, how that handover happens. I mean, in my head, I'm using this is all built on something called actors. I'm using an actress framework in Rust. So really what I'm checking is that I can start that actor. That's what bootstrapping complete means. It's like we've now got a running actor in the system.

1:27:47 So I'll need to work on that. I'm happy with that. Thank you for your help there. That's really good. No problem. Alright. Well, thank you again for joining me, sharing all of your BDD knowledge. I'm really happy we got that scenario completed. I hope it was useful for other people. We've done the whole journey together. We have done a long journey together spread over many many weeks and potentially months but it's it's good because every time we come back to it, we do that recap for ourselves which hopefully you know, is obviously useful for people that are picking up too. So I

1:28:21 think I think we've shown the whole journey from conversation to you've now got a scenario that actually runs. You just make all you do is repeat that now for all the other examples that you've written, have more conversations, and probably write some tests that run the command line up. But it's just different given and whens that just be doing slightly different things. Yeah. Well, I will keep you updated anyway. Hopefully, I can start contributing a bit more to this in a regular cadence and actually making it a bit more useful than just being the hacky

1:28:51 prototype that is. But it has some very nice scenarios and it has some pretty good testing coming up. So again, thank you for your time, Karen. I will speak to you again soon and it's always a pleasure. Have a nice day. Thanks, David. Bye bye.

Technologies featured

Meet the Cast

Weekly Cloud Native insights

Stay ahead in cloud native

Tutorials, deep dives, and curated events. No fluff.

Comments, transcript, and resources

More from Rawkode Live

View all 173 episodes
Rust

More about Rust

View all 22 videos