Database up, the framework and tools in place, TDD process hammered out, all the magic ready--time to cast the spells!
Unit . . . or so I thought
While we will be TDDing our code, we still had to decide on a basic architecture. For instance, if we want to test a Fruit model, well, then we have to decide that we need a Fruit model in the first place. There's a bit of a chicken or the egg problem: we're operating on the principle that the tests will drive production, but yet to even write the test we have to know what we're testing . . .
So, in the example of the Fruit model, what comes first: the model or the tests? The chicken or the egg? The answer: the model. Sort of. In our collective brain it comes first. Tests and production code are dependent on each other. The way the tests are written certainly does determine the way the code is written, but for the tests to even be written in the first place, we have to decide on the basic architecture we're testing. In other words, we have to decide what we're testing.
Jeff and I actually never went ahead and wrote a file without having the tests for it set up first, but we knew in our brain that what we file we would be testing.
So we have to figure out what we're going to need and, perhaps more importantly, what we're not going to need. And that's where we run into YAGNI: You Aren't Gonna Need It.
YAGNI comes from the Extreme Programming methodology and means that we should only write the code that we need now, not what we think we'll need in the future. Yes, we want our code to be extensible, but we also don't want to build out future functionality that is not what we're looking to deliver as a part of our minimum viable product. While this may not seem forward thinking, if we do too much anticipation outside the scope of the functioning product, we'll likely end up writing code that we don't need.
For instance, say eventually user accounts and login is something we want Fruit Cart users to have and do. I go in and install the OAuth library with a Google strategy. Then I start building out functionality for creating a fruit. Suddenly I realize I didn't have a reason for users to log in. Why would they need to log in to create fruits? If all it takes to create fruit is a user account and anyone can have a user account, what is the point of having them log in? Hmmmm . . . seems like for MVP we're only going to need users to create fruit; they don't need an account.
In the future, we may want this functionality if, say, we're going in the direction of an online store. But that's not what we're building at the moment. But, you may say, there is no harm in letting OAuth library or whatever login code we've written hang out in our codebase, so why not? And the answer is twofold: yes, there is a harm, but also, why waste our energy and resources on functionality that is not a part of the task at hand?
First the harm: the more dependencies you have, the more attack surfaces you have for bad actors. Although OAuth is a library we trust, it could be deprecated at some point and/or have vulnerabilities about which we know nothing. Besides, if you're not using it, why is it there?
Also, any code we write does run the risk of tripping up our integration and functional tests. There may be unintended consequences to the way parts of our code interact with each other or subtle changes to the user journey. Keep the tests green!
Second: cost of build. Why spend time on something we're specifically not building out right now? Building new functionality has a cost of time and human labor, so let's focus on what we know we want right now. We can address authorization and log in when we need those features.
For more information on YAGNI, checkout this great Martin Fowler post. He uses a shipping insurance company from Minas Tirith as an example. Hope it fares better than those in Osgiliath.
So let's take a look at the structure of our API so we can even know what-in-the-what we're testing:
ACTION: A call hits an endpoint, let's call it "/fruits", which triggers a database call that has us
getAll()
fruits. Actions and routing to the correct calls will be handled by a controller, FruitCartController.LOGIC: Since all the FruitCartController does is route and return responses, we need to something to handle actual business logic. For that, we have a FruitService. FruitService decides how we're going to get information from the database (not what information--that's decided by the controller). So we're going to have a handy method called
getAllFruits()
which will do just that: get all fruits from the database.DATA: So FruitService is going to call the database using a built-in JPA Repository method called findAll(). This FruitCartRepository is a mapper, and magically transforms data into the Java object of our choosing, in this case Fruit.
That's it.
So let's start at the beginning, with Marcus Aurelius . . . or the FruitCartController.
Because I watched Silence of the Lambs too many times, the quote "What is it in itself", which Hannibal Lector ascribes to Marcus Aurelius, continually pops into my head when trying to test. The full passage from Meditations Book 8 reads: "What is this thing in itself, in its own constitution? What are its elements of substance and material, and of cause? What is its function in the world? What is its duration?"
And in general, these are good things to ask code. What do we want this piece of code to do? What do we need it to do and what pieces of code do we need it to have? How long should it take to do these things? Expectations like these will guide our test-making
Here's what we need from FruitCartController:
To have an endpoint that successfully returns an HTTP response with status of OK.
This endpoint will also need to return a list of all the fruits in the database.
Cool. Let's write a test for the first bullet.
Q: So in an HTTP request, how do we know a response was successful?
A: A status code of 200, or OK.
So we made a test for that in a class called FruitCartControllerTests. Spring provides us with MockMVC, a servlet that mocks HTTP CRUD requests. If you're using IntelliJ (which I highly recommend for Java), you'll be prompted to import the correct library once you start writing code. Our final test reads:
@Test
public void shouldReturnHttpStatusOk() throws Exception {
this.mockMvc.perform(get("/api/fruits").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
And when we run it, it fails. Great: we have our red. Time to get the green.
So now that we have the test, we need to write the code to make it pass.
The obvious file needs to be created (FruitCartController) along with the obvious route ("/fruits"--the "/api" portion is namespacing since we'll also have routing with similar names for the front end and want to be as clear as possible when naming). Then all we do is pass it in the most obvious way, going back to Mr. Aurelius--what is it in itself? What is its behavior? Well, all we're requiring at the moment is that it return a 200 status, so let's go ahead and make an endpoint that does just that.
@Controller
@RestController
@RequestMapping(value="/api")
public class FruitCartController {
@RequestMapping(value="/fruits")
public ResponseEntity getAll(){
return new ResponseEntity(HttpStatus.OK);
}
}
Run those tests and it should pass! Hurray! We have our green.
Now normally we'd refactor, either our test or our code, but in this case, the test is so small, it's hard to see where we would do that. The solution is pretty elegant and does exactly what it needs to do.
So let's take the test one step further; after all, we don't just want a status--we want fruit!
We'll alter our test to require the return of an array of JSON fruit objects. It should look like this:
[{id: 1, name: "apple", description: "A fleshy red fruit"}, {id: 2, name: "banana", description: "A minion's favorite fruit"}]
We'll expect to be able to access the first element in the array and get a name of "apple". Here's the code:
@WebMvcTest(FruitCartController.class)
public class FruitCartControllerTests {
@Autowired
MockMvc mockMvc;
@Test
public void shouldReturnAnArray() throws Exception {
this.mockMvc.perform(get("/api/fruits").accept
(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$").isArray())
.andExpect(MockMvcResultMatchers.jsonPath("$[0].id").value("1"))
.andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("apple"));
}
MockMVC performs that GET request, but on top of the status code of OK, we're also expecting some actual data. So of course, this promptly fails.
Back at red. Let's make it pass.
Getting the test to pass is actually going to be pretty easy: we make a Fruit model with some standard setters, getters, and a constructor, and then just hardcode the response we want directly into the return result in FruitCartController:
@RequestMapping(value="/fruits")
public ResponseEntity getAll(){
Fruit fruit = new Fruit("1", "apple");
return new ResponseEntity(Arrays.asList(fruit), HttpStatus.OK);
}
It's green. Time for the refactor.
But is this really the way we want our system to work? The database isn't being queried, and we'd have to instantiate a ton of different Fruit objects within the controller, which is not its job. No. We need to make that database query to get all fruits.
Now remember in our outline of the system architecture, the controller only handles the routing, telling the rest of the backend what data it actually wants to get. But it doesn't actually get the data. Thus, its ability to fulfill its function depends on another layer of code: the service. Currently, FruitService doesn't even exist; we haven't written the code for it. But it sounds like we might have to start if we're going to get anywhere near how we actually want our controller to work.
And now our little test has moved beyond unit testing. It's an integration test: we're testing that two layers--controllers and services--are interacting appropriately so as to return the desired result.
So let's write some true unit tests; let's create our FruitService.
ABSOLUTE UNIT
Initially we thought FruitService would also be an integration test. After all, it is going to call on the FruitCartRepository, and that would need testing too, right? Well, not so much.
FruitCartRepository, while indeed a file, is an interface. It's a contract between files or layers. And all the functionality we're using is contained by the JPA Repository. So in the end all we have is a file that looks like this:
@Repository
public interface FruitCartRepository extends JpaRepository<Fruit, Integer> {
}
There's nothing inside of it. All we have to do is tell Spring that this interface is a repository (the @Repository
handles that) and basically create it, specifying that it is mapping Fruit data from the fruit table onto Fruit objects and that the primary key in the database is an Integer datatype. All the actual data calls are provided by the library.
So if we were to test our FruitCartRepository, we wouldn't actually be testing any code we wrote. We would be testing the JPA Repository's code. And that, friends, has already been tested. We just need to make sure it's being called right in our own custom code, which means testing the FruitService.
Q: What is the core behavior of FruitService we need right now?
A: We need it to return an array of fruits.
What is the simplest thing it can return? An empty array! Frequently null or empty return values are the easiest way to start with testing. Baby steps.
So we made another test file for our FruitService class and made a test to get an empty array:
public class FruitServiceTests {
FruitService fruitService;
@Before
public void setUp() {
this.fruitService = new FruitService();
}
@Test
public void shouldReturnEmptyArray() {
assertThat(fruitService.getAllFruits(), is(Arrays.asList()));
}
}
First we initialize a new FruitService called, originally enough, fruitService. Then we assert that it calls a method getAllFruits()
and that we expect that value to be an empty array.
Run those tests, and boom: RED LIGHT.
Time for that refactor.
We go in, create that FruitService class, and create a method called getAllFruits()
to return Arrays.asList()
. That's it.
Run those tests, and boom: GREEN LIGHT.
But that's not really what we want, right? We want those fruits!
So we re-write the test to look for a fruit in that array:
@Test
public void shouldReturnArrayOfFruit() {
assertThat(fruitservice.getAllFruits().get(0).getId(), is("1"));
}
That's it. We're trying to get the id from the first element in the array, which should be one.
Run that test, and we're back at red.
Now let's fix our FruitService. We replace the return value of Arrays.asList()
with the following:
Fruit fruit = new Fruit("1", "apple");
return Arrays.asList(fruit);
And that's it: run the test, and the light is green.
But is this really how we want it to do this?
We're running into the same problem with our FruitService that we did with our FruitCartController. It's not querying the database, but creating objects right in its own logic, which is not what it should be doing. It should be querying the mapper (FruitCartRepository), which should query the database to get what it needs. It should then return an array of these objects. So what we need to test for is if it's calling the FruitCartRepository. We want to test it to be sure it's returning not only what we need, but doing it in the way we need it to. And for that we need to up our testing game.
We need a couple of new superheroes: Mocks & Stubs.
Top comments (0)