Sessions is temporarily moving to YouTube, check out all our new videos here.

Writing Tests Like a Speed Demon with Data Builder Patterns

Daniel Sudol speaking at EmberFest in October, 2017
79Views
 
Great talks, fired to your inbox 👌
No junk, no spam, just great talks. Unsubscribe any time.

About this talk

This talk will show you techniques that allow you to write tests without the awful headache and panic that usually precedes this process. Using scenarios, data builders, ascii art and the good old "for loop", Daniel will lay the groundwork for making you a maestro of the testing universe.


Transcript


So I was a little bit nervous about talking because I'm giving up a lot of my top secrets, in testing. It's like sometimes you have your secrets, you've got your things, and it's like, why should I tell you? I don't wanna do, I don't wanna do it. So I was gonna show the cat video. Yeah, yeah, maybe it's better, I don't know, ten minutes of that. But Pat O'Callaghan said, oh come on Dan, just share. So, he kinda convinced me to keep going, so, I'm gonna keep going and talk about this topic. Which is, testing quickly. Sure it says testing like a speed demon but there's a couple of testing like a speed demon. There's a few points to it, there's about six or seven things that I may not get to them all as deeply as I want, there's different levels. Like there's a top level with a databuilder, there's ASCII art, and there's using actions in your integration components and your unit test components. And then below that there's for loops and there's a lot of little tricks that I do, and if I don't get to it, oh well, didn't reveal everything, so. First thing is one of the biggest slowdowns If you're trying to test, how do I even set up my data, and I call that setting up the scene. So let's say we've got this component called the TreeView component. So it's goal in life is just to do that, to make a tree out of blocks. So, right now you're a developer, you're pretty happy, I get to make a tree. I get to make an algorithm to make this tree, set the position, oh this is so fun. Oh we're having a good time, I'm entertained. So, the things you might make with your tree are very simple. You might make a simple tree, one block. You might make a little more complicated tree two or three blocks. Two children, one parent, root at the top. Or you might make three of the same blocks. A blocks, bam, bam, bam, same thing, root with two children. So let's say you wanted, so far it's fun. We've got a TreeView, I get to make a nice algorithm, I've got my tree, I get to put my X, Y positions. SVG, this is great, this is great, we're having a good time, we're having a good time. Okay, hey, about this, write a test. Oh, oh, ah. Set up the data, how do I set up a tree? Okay, uh, I don't know. Starting to lose a little bit of fun here, starting to lose the fun. Magic is kinda, it's fading, it's fading. So, my goals are gonna bring back that magic and get you able to kind of go bam, bam, bam. You can put tests together like, I don't care. Big tree, small tree, I don't care, bam. So, this is what the data, the set up, If you wanted to set up that tree with the A1 at the top, B1, B2. Let's say those are the blocks, I'll get into that later. You wanna set up a test like, hey, the children B1 and B2 are below A1. Simple, I have to make a test. You'll say okay, make block with A type with id of A1, this is just my notation here. You make block, is the model, trait B, ID B1 and so on make the next block and then you put them, assign them, to A1 because they're the two children, and that's your setup. It's not horrible, but it's like, can you do better? Can you make it better? The goal here is to use databuilders and ASCII art to make it better, that much better. So it's a one-liner, and not only is it is a one-liner but it's more readable, it's more obvious, it's more it's just simply shows what your setup is, your data setup is. AndNate is kind of FactoryGuy. and that's, I'll get into more of that later and I guarantee I'll say something controversial I hope. Making fun of Mirage is what I'm gonna do. But I'll get into that controversy later. So let me get warmed up here before I start making fun of Mirage. So TreeView is a component and it has methods that put the block into position. We saw that before, the component puts the block into position. It has a couple of steps that it does where at first it renders the block, the div and it says okay what's the width and height before I can put everything in there it asks my position it's gotta know the width and height so it can say OK. So I'm short-circuiting that step the width part, all right, here's the method, block placement in TreeView. And here's the data model, again, is just block, has many blocks, that's it, pretty simple. The attrs are text and position. One of the concepts that you'll get at the end is this concept of a pallette. I've seen people do these projects where they have these factories and the test data and they're always doing all these random Faker this, Faker that. No, don't use Faker, I use a pallette which has a small limited set of elements. Because I'm trying to say OK, what are my edge cases, what are the minimum number of elements that I need to test all my edge cases. And that becomes your pallette. So, for example I have a couple of block tests, it's gonna be six or seven, ABCDEF let's say. Here is what A looks like. A is 100 by 100 width. And I'm setting it to be, I know that I have an A block it's 100 by 100, bam. It could have different IDs like A1, A2, A3 still, it's a A-type block. B is 150 width, height 100. C is 200 by 100, et cetera. So the Factory is where I define the traits that set up that palette that I'm gonna use. So, I've got a factory, I define the block. I've got the traits are A, so that's unusual. I'm putting the width, I'm putting this class instead of a CSS class. It's sort of a definition, hmm, it's sort of a thing, in the name. To make your DSL, to make your kind of ASCII art, to make your databuilder work, sometimes you've got to do a little bit of hocus-pocus. In this case I knew that the name was gonna be what's gonna be in the template used as the class property, so I though oh, you know what I'll pre-size this block by just boom setting the name to be the CSS class that then sets it to 100 by 100. It's almost like showing the extent of hocus-pocus that I'll do to make this kind of process happen and maybe you're into that, maybe you're not. So then, with the class with 100 by 100 obviously points and makes that. And that's how I set my sizes. So basically I'm skipping the whole sizing step and saying hey, I don't care what the text is you're gonna be 100 by 100. And that's very important, because once again it's limiting. I don't care about the text, I'm not faking that. Boom, this is your size so that when I render a tree this is what I wanna see and this is what I test. This is how the XY position should be. So it's really limiting, it's really perfect, for this kind of thing. So here's an example of an ASCII art concept, here it is, A1, test it app, I wanna make it A1 Scene. So I'd use the databuilder and say hey databuilder build me a scene with A1 and what I said rendered when the template renders that's my scene. So boom, you see it, you get it. So once again it's a TreeView component where you're passing in the blocks, on this case, the block's just A1. Then you've got hey, I've got some ASCII art A1 points to B1,C1 oh let's say I want to test something like that, oh so databuilder, build that scene for me, thanks. I got, there's my tree, that's what you can imagine and visualise if it did render what it would be like, what it would look like. More complicated, and you notice, like even if it's a little more complicated, test it up, about the same, you just slap another line on there, boom. Now you've got the the build scene, it just makes a bigger tree but it's still pretty much So you gotta think it's pretty linear in terms of complexity. Things to note, I'm using the symbols, the ASCII art, to ask you art to build my data. You're creating kind of your own DSL, so that's where kind of the work comes in and the databuilder is generating Ember data models. It's handing you models. There's no JSON funny business, there it is, the data model pushing it into the store and handing it back to you. So what does the databuilder look like? It's got a constructor with a store, so the keys are passing in that store, because obviously you're pushing data into the store, and yeah. Build scene, so this is a little bit of hocus-pocus here. Build scene is going through, when you build the scene you're passing an array of these ASCII art things that build that tree, but I don't know, what does it do what does it do? Do I really wanna tell you? Not really, because it's a lot of, it's sort of, it's a complexity that's based on my, it's my thing, it's my data, it's my tree structure, it's my sort of blocks, it's my models. So it's really up to you how you make it, but you can kinda guess and I'll give you a hint. I've parse each row, I find the trait, I split the ID and then I, hey, do I already have that ID? Bam, hand back the block or not. So in literary form, it would be A1, let's say you've got that ASCII art there, it splits it, finds the trait, gets the ID, A1 trait A, find or create it, bam. And if you've got it, if you've got any children, assign those, and that's it, that's really all it does. So far the builder has been, you say, hey, build the scene. But you can also tell the builder, hey, give me some stuff. Like, hey, give me the blocks that I just made, let's say, if you wanted to just make some stuff and then just hand me back all the blocks, you'd just do something like that. Take a look at that next. So, let's say you've got a test set up like blah, blah, blah, blah, blah, blah, blah, blah, blah, and this is a component unit test for a TreeView and I wanna test. So those are two factors, I've gotta make this a manual setup. I pass the store into the builder, get the component, and the test. So in this case, block placement I wanna test that it can place of I pass it blocks, it will place them properly. The children below the parent. Build the scene. I say block placement, and I say, hey builder, just get 'me the blocks, I could also pass in ABC1 because I know that's what they are, but you know, just to show you what I mean by interaction, just get the stuff. Then I say hey, map the positions, for the Y position, and then assert that the B1 is below, or greater than, in SVG terms, greater is below. And there it is. So, it's kind of a goofy test, it's not really a test I have, because I actually know the exact position it should be based on the size of the block, based on the padding, based on the mid-point bam, bam. You know I could get you know I can get exactitude but for now, here you go, just a basic idea. So one more look at the manual setup versus the databuilder setup. It's not, it's like eh, that's pretty good Dan. More complex, yeah it gets more complex and as you do stuff the the complex one the tricky part is it's a little bit difficult to even figure out what it's doing. It's sort of like a lot of spew. So, whereas the databuilder and the ASCII art you know exactly what you're testing there. You can almost visualise the tree. The databuilder sets you up with this idea of having a pallette, it's not really, you set yourself up, you get the databuilder, you set your limited palette, your small number of items. No bologna, no funny business with Faker. This is it, your limited set and then you start rocking away. So you build up your your tree and whatever so the pros and cons when somebody looks at the left side and the databuilder and the ASCII art, they kind of a almost can see, when they look at the test, they see, oh I got it, it's simple it's concise. It's easier to spot mistakes, because on the right side it's like, I don't know, did I put B, I've made so many mistakes just typos on what am I making, what am I putting together. I don't know, I forgot. And everybody can see what the test is. You can wrap out tests very quickly because it's so simple to build and visualise. These are the pros, but you think of them yourself. The cons are it's a little bit tricky to come up with that, with this concept. And sometimes I'll spend two days, like this one I spent about two days just coming up with how am I gonna do it, how am I gonna do it. OK, got it, bam, bam, bam, bam. But the dividends pay off because as you maintain this, like under the manual setup, after a couple of weeks you're like I don't know, what is that spew? I don't know, it's kinda like, I don't even know what it says. On the right side, on the left side, on the databuilder side, the ASCII art side, it'll last for aeons really, you could pick that up like 30 years, you know, it's like, oh yeah, look at that, oh yeah, I know what that is. New people come in, they know what you're testing, et cetera, et cetera. So, you're thinking I don't know, I don't know, it's pretty good, I'm not that convinced you know, it's pretty good, but show me some more. All right, so let's look at the integration test so. There's an integration component test, what can you do with that? Where you're actually rendering it. You're moving some stuff, doing some mouse events, blah, blah, blah and then something happens. So the top part is mostly just import setup. Manual setup, marks it up, so you're actually gonna do an AJAX, the action, this component has an action that's passed into it, which I didn't show before, and I made, I'll just gloss over it, so here it is. It's got the page object sets up the TreeView and passes in the models. So let's test removing a block. In this case, we can actually test an action. Where you actually have to click on the block, delete it, the Ajax request calls and delete record is called, bam, bam, it comes back and rerenders in tree. So I say okay build the scene, bam, give me A2 and that tree you can picture. I want to delete A2, so what should be left? Should be A1 with child just A3. So hey print the scene will you builder? It's a new concept, so I'm telling to databuilders to print what you see, so the same notation I used to create, I print. I don't know if you could top, that that's pretty good. That kind of test, it's obvious, simple, bam done. So you can just, you could rock out on this all day. Just like hey, can I write another test, please? Okay, can I do another? With this kind of setup and the builders and ASCII arts and the factories, and the making the stuff, whatnot, I could just keep writing tests all day. So, that's part I missed, I skipped. A little bit of hocus-pocus, there's a way, I try, so part of the speed part is not only the databuilder not only ASCII art, but actually being able to test your, some of the interactions, in this case with an integration component test and there's a way to pass in, I have route actions, like the delete model, delete block is actually a route action. There's a way to pass that into the component and in a unit load test or an integration component test and that is the notation there and where does it come from? That I might have to gloss over that, sorry, if you want to know you can ask me later if I gloss too quickly. But the key is you got to see print scene. Wasn't that fun? Great, alright, so things to note. I use FactoryGuy and not mirage. And because I can get fully materialised models, mirages it really isn't really made for sort of putting stuff in a database and getting it back in a sort of AJAX-y style. It's not made for pushing data into the store. FactoryGuy pushes data. I now use mostly integration and component tests because they're so much faster. Later you'll see a test I've got these like tonnes of 200 different unit tests that could be done with an exception test, it would take forever it takes almost no time at all. in the unit test world. It's way faster, yeah, a whole lot faster. We'll get to that if you're not convinced yet. The next example uses, a lot of time you're thinking hey Dan one block model who's got a project one block model? Hey, all right, so let's go saying a little more less simplistic, something like, hey there's a project, the project has groups and that those groups have like department spaces, this is my pallette. It's got a bunch of elements like sales, marketing, playroom and it's gotten those elements have many entries, like user has a blog, blog has comments, then many comments, et cetera. It's all kind of the same thing, bam, bam, bam, there's a tree. And the entry of course has is it part of the elements. So when you visualise this component is the programme tree component, which we'll see later. When it's visualised it's like this, and the data, they way it's set up is there's a project, there's the groups, the elements, each group has elements and then those elements are put into entries in a tree. So I've got to kind of give a little setup of what's going on. The entries are kind of what we're going to be working on but you've gotta, the problem is to test this, to test a lot of the interaction. You've gotta make the entries which are those last, those line items. But you can't do that unless you have a project and you have to have groups and you see all this boilerplate set up, is really a pain, and in this project I hardly had any tests and I had a few that were just so bad. Until I got this concept, and you'll see why, you'll see what happened. So there's my limited set of 10 elements, a couple of groups, and a I call it the pallette. Sometimes I use these little hash maps to go hey, this sale's is in departments, playroom is in spaces, so I sometimes use the little mappings and things to tie things together. So, the program's free, you pass it a project and bam it renders it's programme. It's like a little gify thing here which shows the thing I'm gonna be testing. There's a way that you can take this tree structure it's sort of Excel-y thing but you can move each level to another level and depending on where you move it, if you take a row see what's going now straight, yeah, so you take a row I see now and you move it in between two it'll put itself in between those. And this is a little bit complex, this example, but let's see what that one does. Yeah, all right, so fine. It's probably hard to see exactly what's going on, but the basic idea is if you move on top of another entry it'll put you as the last child. If you move it in-between it'll basically just slot you into that sibling. So stop staring at that. So on top goes down, to the if you move on top you go to the bottom, as a child, you go down to the bottom, last child. If you move in between, you become a sibling. So that interaction there, somebody this when companies are like yeah, we want that, we want that, we want to be able to drag stuff, I was freaking out. Oh my God are you crazy, I'm not doing that. I'm not doing that, forget it. Because that features, just because I'd realised to test that, nightmare, total nightmare. If you did that in acceptance tests, like, poof, you're toast man, I have about 200 different edge cases you'd be standing there to Thanksgiving just like running through it. Okay, fine, it's only 10 minutes but these all run under a second, the 200 edge cases. So the USED set up used to be like that. Need I say more? So, all right, moving on. So the new ASCII art notation for that would be there. This is a simpler version, almost what I had before but just very, about the same, that setup is the same as what you saw before. Just a little simple programme, boom, sales, marketing in a room. So when you build that scene builder what it does is it makes that, nam bam bam, so the ASCII art is a little bit different but it's a tree but it's a different style of building so I using padding basically, little pads to show that how the tree is a parent-child, child. Now now I can actually get different parts of that tree but hey get me the sale one which is the top one I could say sales market so I'm kind of saying like parent-child, child I can go down with a different DSL to get the different entries. Like that, so here's the test, I want to move an entry, like we're seeing before, so I want to test that I drop an entry on top of another entry and it becomes its last child. All right, so let's see what I'm gonna do. So I've got my original scene that I want to build I've got common room, has a playroom, is in common room, conference has conference rooms, and that has cubicles, 6x6, and there's a sales and marketing which is a sibling if they're all on the top level and there it is, there's my test set up, I'm done. You can kind of picture what that's gonna look like. So what, oh yeah, we got to move something, yeah. So I wanna move, the yeah I want to move marketing the one at the bottom. I want to move it up to in between, I want to move it right COMM, CONF which is that one so, COMM, CONF, boom. so I'm gonna right on top of there and as we said before like hey I expect when I suppose happen again, suppose now the tree should look like, yeah playroom COMM, CONF, yeah, yeah, yeah, CU66 is when you put it on top of something, it goes as the last child, yep, there it is. So market's there and sales, so market is now it is right where it's supposed to be. It's using asynch await so actually it's a way to why am I using asynch await in a unit component test test because it's actually running the AJAX call. So I've passed in the action, the route action that does the move and it's gonna wait on that. So what is verify look like? Run it as a unit component test. So, it's an async method, it gets the original scene, the move and the expected scene. Hey builder, make the original scene, done it, done that before. The move is like that, the move entry the over entries, I use the Builder hey give me just flip through map, give me the model, give me the data models for the move and the over entries and pass it right into that method drop entry. one of the key elements the key things, is when you make actions, when you make methods, in your components never avoid the magic of having sort of the component action that goes hey get this and get that always make your actions receive the parameters that they need to as as much as possible that they can. In this case the only magic was, I had on the component, had passed in the closure action. That's it, everything else is passed indirectly to that act, to that method, which is drop entry. So, that means you can test it like this and say oh the print the scene it should equal what I expect, for verifies, so. So, the magic here was, this is a little bit of a hint of how I did it, how did I pass the actions into that unit component test? This is what you would do in a an integration test but I used the, alright I don't want to show you that. Do you want to see that? No? Good. With a validation drop, this is a little bit, simpler because it's not even doing any AJAX calls it's just saying hey when I move, or when I'm dragging stuff around is that a valid drag, is that a not valid drag? And that's happening a lot because it's got, it lights up green if it's valid. And it's doesn't light up at all, all if's not. And those are super important to the user experience because it's just to make this work. That was a very important thing so I set up my programme as before and then I say, OK. I'm using it I'm using this array it's interesting so what if I moved so that the idea here is I'm setting up all my edge cases. What if I moved common room I start with common I moved it in between sale and market. Is that valid? In this case, yes, I'm saying it's true and it's because it's a sibling resort, so that's a valid kind of you could do. So the second case was if I move community of the common room. this the Cbe c6 in the common room on top of market is that valid no because already has that entry. Let's say you don't know I'm talking about. let's say you do, great. The point is I can just whip through all my edge cases And then for the solid entry drop, I just use a for loop to just whip through them all and verify them. See that before in the verify method? was asking for those entries back the models and I passed the models right into the valid entry drop method. Once again, the method is always, it takes exactly what it needs, no magic, but in that method except, give me what you're asking for, and I'll tell you if it's valid, and that's it. I'll tell you if it's valid, and that's it. So I went through I think about 50 of those if somebody says and this is the only that features the most complex probably ever done, and it was bug free when I've shipped it, except one guy found two bugs and how long did it take me to, uh, create those two edge cases, I forget. So how long it take me to add this Edge cased? I don't know, five minutes? I went back and I go yeah, I forgot that you can move the comma in front if the thing, added those, done, never heard from that guy again. So anybody, again on this feature which is ridiculous. Oops, so in conclusion, I had to prove to myself that I need the data builder, but I don't know if I proved it to you exactly. I guess the key thing is if you're going to test quickly you want to stay, I happen to stay out of the realm of acceptance tests. I use them just for the happy path. So, for example, in that programme tree, sure, I'm gonna acceptance test it takes the thing, grabs the thing, moves it around, drops it, just to prove the the actual dropping the grabbing stuff works to the droppy stuff works. Things are happening there's a, the route action is there I just do that once and the rest, every edge cases handles in a unit or an integration component test. and that's kind of what, that's one key Two is the for loop concept where you just whip through all your cases unbelievable speed up that way, the databuilder and the the ASCII art concept wow. I mean I couldn't even live without it now. So, that's it, thanks