Check out a free preview of the full Introduction to Node.js, v3 course
The "Testing with Mocks" Lesson is part of the full, Introduction to Node.js, v3 course featured in this preview video. Here's what you'd learn in this lesson:
Scott discusses testing with mock functions and data to perform tests without modifying the actual data in the database. Mock functions, also referred to as "spies," observe the behavior of indirectly called functions, allowing for thorough testing of function behavior and output.
Transcript from the "Testing with Mocks" Lesson
[00:00:00]
>> Okay, cool, that's testing. So let's write some real tests for our notes. Like I said, we're not gonna write a test for everything, but you'll get the point. So if you scroll down here, I actually have some stuff. So we're gonna copy this and paste it and talk about it, cuz this, actually let me copy and paste this whole part right here, cuz we'll talk about this.
[00:00:23]
A couple of things going on here with this one. So because we're using type module in Node.js, which is relatively new, a lot of things are still reacting to that change. The module syntax isn't just some syntactical sugar. It's actually a completely different change in functionality. So in testing, there's something called mocking.
[00:00:46]
To mock something is to basically replace it with a stub or an alternative version, usually a function that does nothing. And the reason we mock things is because we are unit testing something that depends on something else, but we don't want to test this something else. We just wanna test the thing that's using something else, so we wanna mock out or stub out the something else.
[00:01:08]
In this case, the something else for us are the database functions that we wrote. We want to mock those out. We don't want to write to the file and read from the file when we run our test. Imagine writing to a database and reading from a database every time you run a test, which, actually, some people do, some people just spin up a test database that does that.
[00:01:28]
But if you just mock that database out and just return a fake information that the database would return, then you don't need to spin up a database. You can just mock it out, because you don't need to test to see if the database is saving, that's the job of the people who made the database, is to make sure it saves you, so you shouldn't be testing their code.
[00:01:46]
So in our case, we're mocking out the database helpers that we made. So in order to do that with Jest because ESM modules are new in Node, we have to import the Jest global and use this unstable_mockModule syntax. So basically what this is doing is, it's saying, go this path, I want to mock out all the things that got exported there with these Jest functions.
[00:02:13]
This Jest function are just functions that do nothing, but return what's called a spy. A spy is basically a function that will tell you everything that happened to it. It'll tell you how many times it got called, what arguments it received, who called it, and that way you can do expectations on that.
[00:02:30]
So if you need to write a test that says, I expect insertDB to have been called three times with these arguments, you can do that, cuz there's a spy there now, so we have to mock it out. And here's where it gets a little crazier. These are called dynamic imports or async imports.
[00:02:47]
So you can use the import as a function and put await in front of it, and you can dynamically import it. And the reason we have to do this is because we need to mock these things first, and then we have to import them dynamically. If we import them statically, we won't be able to mock them, at least not in the new ESM syntax.
[00:03:08]
With CommonJS, this is a one-liner, we'll be done. This is super complicated with this. So you're probably learning something that no one at your company probably has ever seen. So this is relatively new. Okay, so now that's out of the way. We mocked out our database, we brought in the database stuff, and we brought in all the functions that we're gonna test.
[00:03:28]
I'm just gonna test these three functions here. And then the last thing I wanna do is, let me change my names. My names on here are different. The last thing we wanna do is that we get this really cool beforeEach function, and basically this function will run before each test.
[00:03:49]
So if we write three tests before the next test runs, it'll basically clear the mocks out basically. That's basically what we're saying. So you can put whatever you want here. This might be where you tear down the database, insert something new into the database. When you write tests, you wanna make sure that each test has its own fresh state.
[00:04:06]
You don't want tests to be stateful, as in they share the result of the previous test. That means your tests have to run in a certain order. If you move this test around, it'll break. So you want the test to be completely modular and stateless. So this is us clearing up any state that we might have added by clearing out the mock, so the next test is fresh.
[00:04:27]
You will run into problems if you don't do something like this. Okay, that was a lot, let's write some tests. [LAUGH] So first one, let's write a test for insertDB. So we're gonna test insertDB, actually, I wanna do exactly what I had here. What did I have here?
[00:04:46]
Okay, let's do this one. So newNote inserts data and returns it. So let's try that. So newNote Inserts data and returns it. It's gonna be async cuz all our functions are async, so you could do async here, it's really cool. And then basically all we're gonna do, I'm just gonna explain it, the strategy behind it.
[00:05:13]
So we're gonna make some note content, we're gonna make some tags, we're gonna make a new note object, and we're gonna use the mocked out insertDB function that we made. This is the mocked version of it, and pass it there. So it's mocking out what it would do.
[00:05:27]
In fact, it's not gonna do anything, but that's besides the point. The reason we had to do that is because newNote is going to call this function, right? And I know that cuz if we go look at it, where's notes? If we go look at newNotes, it calls insertDB.
[00:05:44]
So when this gets called in newNote, it won't be this insertDB that gets called, it will be this mocked insertDB that gets called, right? That's why we have to add that mock there. So make our note, add the mocked value there, resolved value. This means that insert's gonna return a promise, so we have to resolve it to this data.
[00:06:10]
Now call our function, and then we can expect the results to equal whatever. In this case, we know that newNote returns the note that you gave it, so it should be the same. So we can just test that, so let's give that a try. So let's say content, actually, I'm just gonna say to do it all on one line, newNote, content: 'this is my note', id is 1, and tags are ['hello'].
[00:06:39]
So we have that. And then the next thing we're going to do is, like I said, we're gonna do the mockResolvedValue, so I'm gonna say insertDB.mockResolvedValue, with a new note, like that. Then we're going to call our function with the note and the tags, so let's do that.
[00:07:10]
So we'll say result = await newNote, with the note or the newNote.content and then the newNote.tags. So we got a result, and now we can just say expect. What did it import? Don't import that. Expect(results).to, so toEqual is different than toBe and I'll talk about that in a minute, toEqual, newNote. Okay, so toEqual, it's not a deep check, it's not a strict check.
[00:08:00]
It's just checking to see that these two things have the same properties on them, not the same two things in memory, cuz that would probably fail, or, in this case, maybe it won't because I used the same object. If you did toBe with the two different objects with the same properties, it would fail, cuz they're not the same object in memory.
[00:08:14]
You can't triple equals two objects, it will always fail. I don't know if you guys knew that. Like if I did, node, is that true or false? It's false, cuz there are two different objects in memory. So again, this is because we're using just this one line right here, "type": "module".
[00:08:38]
Because of that, we have to run our jest command like this in order for it to respect the module syntax. So let's replace jest in our scripts with this. So it's gonna be --experimental-vm-modules and then space node_modules/jest/bin/jest.js, which is the path to the installation of the jest that we just installed in our node modules folder, you can see it here.
[00:09:21]
So we have to do that in order to use the modules, and that's why it's freaking out. If you don't wanna write all that down, just go to the repo. The repo is at the first page of the notes. Click on that, do what I did. Go to the package.json and you can just copy that.
[00:09:39]
Okay, so now, let's try to test this again. And cool, at least it ran this time. So now it's saying newNote is not a function. I must have either not imported it or called it something else, newNote. Well, it's probably because I called this newNote, right? Let's call this note and call this note, there we go.
[00:10:03]
And we'll call this note, And also this note and this note. Okay, try to run that again. All right, getting closer. So now it's saying, Everything looks good except for the IDs. And I think that's because, I think newNote makes an ID, right? Let's go see. Yeah, newNote already makes an ID, so clearly, [LAUGH] I can't expect it to be the same thing because newNote already makes an ID.
[00:10:49]
So what we can do is we can just test the properties that we have influence on, which are not gonna be the IDs, right? So the way we could do this, and there's more than one way to do it, but we could just say, (result.content).toEqual(note.content), all right? And then you can write another expect, (result.tags).toEquals(note.tags).
[00:11:15]
Let's see how it likes that. Cool, and then that passes.
>> Should unit testing be done from a technical point or from a user perspective?
>> That's a great question. There's different philosophies on how you test, and there's difference between BDD, which is behavioral-driven design. There's TDD, which is test-driven development.
[00:11:44]
Or I'm sorry, BDD is behavioral-driven development. TDD is test-driven development. I think it really depends. It depends on the organization. I like to think of things from, when I think of unit testing, I don't think about the user, but when I think of end-to-end testing and integration testing, yes, I do think about behaviors, and I like to test those behaviors.
[00:12:05]
Because at the end of the day, my app is just a finite, unless again, you're making a video game, even then it's pretty finite, but your app is just a finite set of behaviors. So if you can test every behavior and try to catch all the edge cases for each behavior, then you should feel pretty confident that your app's gonna do well.
[00:12:24]
So yeah, definitely wanna test for those. As you get closer to something that's user facing, you wanna test user behavior. But for things that are so abstracted away, like a function like this, it's hard to associate that with a user, because it's a function that knows nothing about a user, it's just a very small unit.
[00:12:42]
So yes and no, it depends.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops