Check out a free preview of the full Web App Testing & Tools course
The "Structuring Code for Testability" Lesson is part of the full, Web App Testing & Tools course featured in this preview video. Here's what you'd learn in this lesson:
Miško emphasizes testing is all about correctly structuring your code. Leveraging patterns similar to dependency injection or a more functional style will simplify the inputs and outputs of a function and make it easy to test.
Transcript from the "Structuring Code for Testability" Lesson
[00:00:00]
>> This is not obvious at first when you start writing code, you think of testing as something you do later to the codebase rather than as being part of the process of writing code. And I think this is an important kind of thing to to internalize. And it's not just that, it's also the thing I talked about, which is, you wanna set up your environment so you don't have to think about it.
[00:00:27]
When somebody comes to you and says, x is broken and you wanna reproduce it, you immediately say, the easiest way for me to reproduce it is to write a mini-test, right? If somebody comes in and says, the clustering isn't working the way I expect, you should immediately be like, okay, let me write a unit test with the thing you're describing see if I can produce the thing.
[00:00:50]
And the easiest way to do it is inside of a unit test because it runs automatically on a side I can immediately see the output, I can see what's happening. I can run it under debugger to figure out what the issue is and so on. So I always say that testing is not like frosting, meaning that, frosting is the sugar you put on when you bake, right, but flour is the ingredient that has to go into your cake.
[00:01:17]
Like if you skip flour when you're baking a cake, whatever you get out of it, right, like you can't add flour on top of it afterwards and expect to have a cake, it just doesn't work, right? So testing is not like frosting, you can't just add it after the fact, it has to be part of the process.
[00:01:35]
So the other thing I wanna talk about, and I think it's useful as a mental exercise, is to say, suppose you're an evil engineer and you wanna make your code hard to test, what do you do, right? Because presumably, if you can answer that question, then you can answer the question of what not to do to make your code easy to test.
[00:01:57]
And I have a class for you here. This happens to be Java cuz I wrote this a while ago. And when I show this to people, what usually happens is that people start reading the code and they're trying to understand what it does. And I think that misses the point, because what the code does has really nothing to do with whether or not it's easy to read or write.
[00:02:24]
As a matter of fact, we have minifiers, we could minify the whole thing.
>> [LAUGH]
>> Right, and now you have no idea what the code does. And yet I'm gonna argue that the ability to test it has not changed, right? So testing the code has nothing to do with what the code does and it has to do everything with how the code is structured.
[00:02:46]
And we kind of touched up on the biggest part of this, which is that you need to be able to get hold of your dependencies. And so let's color code this a little bit. So this is the green part over here. And what the green part represents are variables that I can control because I can pass in a mock, a friendly, a fake implementation, a database.
[00:03:16]
If the data presents a database, I can pass in a local database over here, I control D, right? And because I control D, if I try hard enough, maybe, maybe I can control B and C. But I say, if try I hard enough, meaning, if I'm really clever about what green color represents, the yellow color becomes one step removed, right?
[00:03:44]
So if I control the green color directly, I may be able to get hold of the yellow color because you can see that the green is used as an argument in there. So if I mark enough things, maybe I can get a hold of the B and the yellow stuff, does that make sense?
[00:03:59]
And then if I try hard enough, maybe I can get a hold of the third level of indirection. But that's about it, at that point, your tests are gonna look so ridiculous, and you're gonna have so many mocks returning mocks, etc, that the whole thing becomes completely unmaintainable and ununderstandable.
[00:04:20]
So the way to look at whether or not code is testable is to ask yourself a question of, what do I get to control, what is under my control as a developer? And if I can control things, then I can test them. And if I cannot control things, then I cannot test them.
[00:04:39]
So the point of refactoring when you're doing test, is to rearrange the code in such a way, so that you can you can get hold of as many things inside of these classes as possible. That's why when I was talking about it earlier, I said, hey typically we write code is when we mix business logic together with the construction lodging of how we instantiate all the functions and classes, etc.
[00:05:06]
And those two things need to be separate. I need to be able to build the system. You can think of it as your system is like Lego blocks that you wanna put together in a particular way to have a specific kind of application working, right? And LEGO pieces are usually, some are general, but some are very specific to do very specific things.
[00:05:26]
You wanna make sure that you can get hold of and play with each LEGO piece independently, so that you can verify that every single one of them is doing the right stuff. So if you refactor the same exact code, if you believe that it's the same thing, notice all I did is I moved things around so that the constructor, instead of looking for things, is just handed things.
[00:05:53]
So let me go back up a second. If you looked over here, you'll notice what's inside of the constructor here. You were given stuff and instead of using it, the constructor did work, it looked for other things and made more stuff, right? Whereas if you refactor it, the constructor is all nice and green, it doesn't really do anything, it just stores the stuff that you were given.
[00:06:16]
Again, let's go back to where we were writing the GitHub API test, we were doing the same exact thing, right? We wanted to make sure that the GitHub API, we could sandwich it. And so we wanted to make sure that I can pass in the fetch function, I could pass in the delay function.
[00:06:34]
I wanted to make sure that I can control both sides of the of the GitHub API. And the way we did that is by passing things in. Now, you can look at it in terms of the object-oriented world, which is the constructor passing, but if you think about what JavaScript is, JavaScript has functions, and functions have closures.
[00:06:57]
And closures are essentially the same thing as a constructor, they just close over things. And so you wanna make sure that you can call that function that closes over stuff and you control what it closes over. And as long as you can control what it closes over, it is equivalent to having the control over a constructor, right?
[00:07:16]
So like our GitHub API was written as a class, it didn't have to be, it could have been written as a functional thing where you close over other things. We would just have to make sure that we can close over a fake phage and a fake delay so that we can control the time and we can control what goes over the wire to the actual GitHub, and the same thing would still apply.
[00:07:37]
>> Yeah, so speaking of Java, sometimes you have to providers and dependency injection and all of that. So do you test for your providers? And,
>> Very good question. We're gonna actually cover more of it a little later, but the short answer is, your end-to-end test verifies that everything is wired correctly, right?
[00:08:03]
Because what you're doing with the providers are using wiring things up in a correct way. If you can do a basic operation on an end-to-end test, then you know that all the pieces must have been wired incorrectly, right? So the end-to-end tests tend to answer the question is, are the pieces wired up correctly.
[00:08:20]
Whereas a unit test, answer the question, are the individual pieces doing the right stuff? And you can have everything in between. Yes, Mark.
>> You're saying that the more decoupled our code is the better it is?
>> That seems like somebody's,
>> [LAUGH]
>> What's the word I'm looking for here?
[00:08:39]
[LAUGH] Intentionally said that to kinda make a point, yes, that's literally what I'm saying, yes.
>> [LAUGH]
>> Sounds like you're optimizing for dependency injection. What's your take on dependency injection, especially when applied to JavaScript and TypeScript?
>> Right, so dependency injection is just a fancy way of saying that, I wanna be able to control my dependencies, right?
[00:09:06]
What we did with GitHub API was that dependency injection? I guess, right, we were controlling how we pass things in. And so again, I'm just gonna come back to the original statement is, I don't care how you control your dependencies, as long as you can control your dependencies, right?
[00:09:25]
If you have a system where you cannot get a hold of the fetch and you cannot get a hold of the delay, you're gonna have a really hard time being able to write these tests. Whereas if you can't get a hold of these things, you can do it.
[00:09:42]
Now, Javascript, because it's duck typed, which is dynamically typed, right? What you can do is you can always get a hold of, for example, fetch because fetch is just a property on global space and you can replace it with something else. So you always have kind of a heavy handed way of getting hold of your dependency by patching the global state.
[00:10:09]
But global state is problematic, so, in my opinion, it's always nicer to be able to do it in an explicit fashion rather than to do it in kind of the global patchway. Primarily, because if you globally patching a fetch, then you can only run one test at a time, you cannot do parallelism, right, because if that test is running, then no other test can be running.
[00:10:34]
Now, you can argue that in Javascript it doesn't really matter because Javascript is single-threaded. With promises and the weights, you can kind of get multiple tests running concurrently. Not quite the same thing, so you have that argument. But at the end of the day, it's always much easier to reason about the test.
[00:10:54]
When I'm explicitly passing a fake fetch, I look at the test and I'm, they're passing a fake fetch. Whereas if you don't do that, then it's like some setup code somewhere ran and it replaced the global fetch with this other fake one. And now if I call this magical function, I can control what the fetch returns, but it's not easy for me to see how all the pieces come together.
[00:11:18]
So it's possible to do it without it, and it's possible to do it without dependency injection is just explicit DI makes it easier. And again, a lot of people will think of dependency injection as object oriented with constructor, but I'm gonna argue that just making a function that closes over your dependencies, that is also the same exact thing as just an object-oriented class that has a constructor.
[00:11:45]
It's just that the syntax is much cleaner and simpler with functions and closing over variables. Hopefully, I've convinced you that what the code does is irrelevant to whether or not it's testable, right? It's all about how the code is structured that makes it possible to write unit tests.
[00:12:04]
And this is why, I go back to this thing that there's nothing I can teach you about writing tests, I can only teach you stuff about writing testable code. So it's all about how you structure your code, not some magic of using some specific tool etc. Tools are useful, and they do a lot of interesting things, but it's all about structuring the code in the right way.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops