Productionize Your Database Seeder in Cypress

Brett Cassette
InstructorBrett Cassette
Share this video with your friends

Social Share Links

Send Tweet
Published 6 years ago
Updated 4 years ago

Our database seeder needs to be extended to work in production. It should reset between tests, auto-increment ID sequences, and allow us to set defaults on our seeds to keep our tests easy to maintain.

Let's extend the example from the previous lesson to get to a production ready seeding abstraction.

Instructor: [0:00] In the last lesson, we saw how to make a basic database seeding abstraction that's suitable to our test app. In the real world, we'll want something a little more robust. For instance, it would allow us to set defaults. Completed is false by default, so it doesn't make sense to have to specify that every single time.

[0:18] Next, ID numbers will be a little bit tedious to maintain. It would make more sense for these to be auto-generated for us. Finally, if we wanted to override an attribute, like we need completed to be true in one case, then we just need to specify that, and our abstraction will respect it.

[0:35] One way to think about what we're aiming at is that we want our abstraction to provide some sensible defaults while still giving the end user complete control to override whatever they'd like.

[0:45] Let's make a new abstraction called a factory. A factory will define the defaults for each of our object types. We can make a manifest file under Cypress. We'll make a new folder called factories, and then a new file called factories.js. This will be our manifest.

[1:05] We'll say module.exports equals, and then create a hash. Our manifest file will simply require the related files that it needs. We know that we'll define to-dos under to-dos.js. This is where we'll define our defaults. We'll say module.exports. We'll say the default text is "Hello World." The default completed status is false.

[1:35] We want our seeding abstraction to support these factory defaults. Let's open up Cypress, support commands. We'll create a new Cypress command to do this. Let's go ahead and scroll to the top of our file, and require our manifest file as const factories equals require, and then require our manifest.

[1:58] Let's add our new Cypress command. It's called seed. It accepts a hash of seeds, as well as some options, so we configure it not to log if we don't want it to. In order to apply our defaults, which we'll call map seeds, we'll need to reduce our seeds. The reducer function will have an output. That's the seeds with the defaults applied.

[2:21] It will have an array of seeds that we're currently applying defaults to. It will have a current key, which is the name of the model, aka to-dos in this case. Then we need to pass in a hash that will serve as our output to the function, which will hold the seeds with defaults applied.

[2:39] Let's start writing the reducer function. First, we find the factory for the current model. We'll use our manifest file to look up the correct factory. In this case, it's the to-dos factory. Otherwise, we can explicitly label that the factory is undefined.

[2:55] If the factory is undefined, we'll go ahead and specify that the seeds with defaults applied is just the user-specified seeds because we haven't defined any factory defaults yet. If we have defined a factory, we'll go ahead and map the seeds. We'll use lodash.defaults to apply the factory defaults.

[3:23] If options.log is not false, then we'll log out to Cypress. The name of the log is seed. Message is the seeds with defaults applied. Console props, same thing.

[3:41] Finally, we'll actually call our CY task that we defined in the last episode. We pass it the seeds with defaults applied. We make sure it doesn't log because we already created our own logging functionality here. Great.

[3:56] Let's go ahead and use our new command in our test. We can pop back to our test file, change to cy.seed. No longer need to call the task. Let's delete everything that's not default. Obviously, we haven't set primary keys yet, so we still have to add a primary key.

[4:13] Let's do this and reopen Cypress. We'll see that we have the text, "Hello World," which was our default. We don't have completed set. That's great.

[4:23] We can override completed and see that that should override it for us. We see that it does, awesome.

[4:31] The one thing we have left to do is figure out primary keys. Let's look into that.

[4:36] Let's pop up in our console. We'll create a generator function. A generator function is a function that can stop midway through, and then continue where it left off. We ca use a generator to generate a series of numbers that continues increasing.

[4:54] First, we'll define the ID variable. This is the variable that we're going to increment.

[4:59] Next, we'll define a loop that never exits. Whenever we call our generator, it will always yield the next number in the series. We'll say while true, then this is where we pause the function. We'll say yield when we're yielding the next number in the series, and then we pause. Next time, when we run the function, we un-pause and yield the next value, then re-pause, and repeat over and over.

[5:24] Finally, we'll just check if the value has gotten too large, at which point, we'll just reset back to zero so it will go up to whatever this is -- 10,000, 100,000 -- and then it will go back to zero and start over again. We can just test this out and show that it works.

[5:46] Let's hop back into our commands and create a hash of generators. We'll look through each of the keys of the factories and just create a new one for each.

[5:55] Let's paste in our generator function. We'll say _.each. For each of the factories, we'll get the factory and the key, and we'll say generators, key equals and then we'll just make a new generator function.

[6:14] Then we'll come down here to the location where we'll actually use this function. We'll keep applying the defaults in order. We'll just add an ID. We'll say that that is the generator for this particular. We'll call .next.value. That will give us that value.

[6:38] We hop back into our test. We can just delete any of these defaults now. We can create as second to-do. We'll just say that it has the text, "Hello World," too.

[6:49] We can copy out this assertion. We'll just change its primary key and its text to "Hello World," too.

[7:00] Again, it's getting this primary key two and one just based on the order these are created in. We'll say that the length of the to-do list will now be two. Go ahead and update this to be "Hello World" for a default.

[7:13] Let's run our test. We'll see that it passes.

[7:17] We just have a few more hiccups that we want to think about. For instance, if we run two versions of this test back to back, but let's say we comment out our cy.seed at the start of the second test, which means that we shouldn't have any to-dos, we should have an empty database in the second test, we go and see actually that the second test passed and we do have all of our to-dos loaded in.

[7:43] That's because we haven't reset the database in between every test. If we want to reset the database, what I like to do is go into our commands. We'll say beforeEach, and then in the beforeEach, we'll call cy.seed.

[7:59] Then we'll pass it our reset state. We can take a look at that. We'll see that the second time, we did actually seed an empty to-dos array.

[8:10] We do that actually the first time as well. We seed the empty to-dos array, but then in the test, we run our own seeds. That's what actually gets us the data that we want. That resets us to a totally blank state, which is great.

[8:27] The next thing to think about is that if we do call two versions of this test back to back, the second one will fail. The reason for that is because we actually keep the generator function running. Instead of this being ID number one, it ends up being ID number three.

[8:47] We don't want that because we want each of these tests to be totally isolated from each other. It shouldn't matter whether both of these tests run one after another. We want them to be independent. What we have to do for that is to reset our generator.

[9:06] Let's go back up here. We can create a function called reset generators. In here, just go ahead and paste this reset.

[9:19] Then in our beforeEach, we'll just make sure that we also call reset generators. We'll reset those generators between each test. We have true isolation, a much more robust, and production-ready database seeding tool.