Functional Testing22:05 with Craig Dennis
Let's take a swing at using Functional Testing of our API. We want to make sure we have status and the correct JSON response.
So it is time to write some functional tests. 0:00 Are you ready, I know I am. 0:03 So typically these tests are written to prove 0:05 to people who requested features that things are operating as they expect. 0:08 So what we want to prove here is that when someone makes a web request to 0:13 a specific URI or resource, that certain actions happens and 0:18 the correct status codes are returned. 0:22 Right, so that's all we're testing here functionally. 0:25 Now, what makes things a little hard here in Spark land is that our routes 0:28 are lambdas as well as they accept some of these carefully request and 0:32 response objects, right? 0:37 Some frameworks provide a handy way to perform this testing. 0:38 But alas our little little microframework here does not. 0:42 So like we often have to do in microframework land, 0:45 we have to craft our own solution. 0:48 So let's start by making a test right? 0:50 So we'll come here to this API and we will do command shift t. 0:52 We'll say create test, That's great, let's go ahead and do an before and an after. 0:57 We know we're going to need those later and we'll click OK. 1:04 Okay, so one thing I wanna do here is actually start our sever up and 1:09 the way we do that is by calling the main method right, in our API class. 1:13 We're gonna keep our server running for all of these tests, all right, 1:17 we're not gonna start it and stop it. 1:20 So we're going to use an annotation called BeforeClass. 1:22 So this will run once before any test run and only once after it's all done. 1:26 So we're gonna call it BeforeClass and it's going to be a static class, or 1:30 a static method sorry. 1:35 And it's gonna be called startServer cuz you can name whatever you want, 1:37 we want to make sense. 1:40 So here we're just going to call that main methods, we're going to say Api.main. 1:41 Okay and it takes arguments, and right now we don't have any. 1:47 So we're gonna say null and 1:50 remember that's an array of strings that that accepts. 1:52 So let's also stop it when we are done, so we 'll say AfterClass. 1:56 And again we can name this whatever we want but 2:01 I think it's probably best if we call it stopServer, seems to make sense. 2:03 It's clear, anybody can look at that and know what that is. 2:07 And there is a static method off of Spark called stop. 2:09 And it's talking about, we're talking about Spark Spark. 2:16 [SOUND] Okay, so let's take a quick look at what this is gonna do. 2:20 If we flip over to our Api.java and we look in here, this is the main method 2:23 that it's gonna call and it's gonna create a connection to the database. 2:29 And it's the file database, 2:34 that means any time our test runs it would write in there. 2:35 We don't wanna do that, that's no good. 2:37 We need some way to configure this. 2:40 You know what the other thing is? 2:43 This is always gonna run on port four, five, six, seven, but 2:44 we know that if you try to run the server while the server is running, it blows up. 2:48 So we can't do that, we need to configure both of those. 2:53 So we need some way to do this. 2:55 Now just so you know, there are great ways to do this. 2:58 There's third-party configuration libraries out there, 3:01 Apache Commons Configuration is the most popular one. 3:04 Check the teacher's notes for some more of these. 3:07 But let's go ahead and do something pretty simple here. 3:08 And later we can implement one of those custom configurations if we wanted to. 3:11 So we do have a way to push settings into our API and 3:15 that's through the arguments passed in, right? 3:17 We can pass something in there because we're calling it on the other side. 3:20 So let's do that, let's come over back to the test. 3:24 And where we're passing something here, let's go ahead and 3:29 let's make a new array here. 3:33 We'll say, string of and we'll call it args, we call it whatever we want. 3:34 And let's go ahead and let's set the port. 3:41 We'll set the string, so we're gonna put it in there, we'll say 4568 for the port. 3:43 And let's set up the jdbc:h2, we want in memory for sure, we want that to be fast. 3:48 And we'll just name it testing like we did before. 3:55 So, there's that and we'll pass args in. 3:57 You know what, this number looks a little magic doesn't it? 4:01 Let's go ahead and that's always a bad thing. 4:03 So let's go ahead and we'll choose Refactor, and we will Extract, Constant. 4:06 And we wanna this PORT, and we'll go ahead and also extract the constant here. 4:14 So we'll do Refactor, Extract, Constant. 4:23 And we are going to call this TEST_DATASOURCE just to be super clear. 4:28 Cool, and if we look up at the top here those are exposed and 4:36 now somebody reading this understands what it is that we are doing. 4:39 And you can change it in one place should we use this elsewhere. 4:41 Okay, so now in our API class we have some arguments that are passed in right? 4:46 So why don't we right here say if args.length, 4:51 cuz you don't have to pass anything in and if we don't we want it to run like normal. 4:55 So if there are args.length we know that the first one is the port, right, so 5:02 we'll say port. 5:07 And we will do Integer.parse, because we passed in a string, args. 5:09 And port is a static method off of Spark. 5:16 So, that will set the port that the server runs on. 5:22 Now the second parameter is a little bit trickier and actually, why don't we move 5:25 this right up by the arguments, that's kind of the best practice. 5:28 So let's move that above these guys here. 5:32 The second argument is the data source, right? 5:36 So, we're gonna go ahead and let's set what the default data source is. 5:38 So, I'm gonna grab this. 5:42 And we will pop that up here, we'll say String datasource. 5:45 Okay, so since there are two, we're gonna set specifically 5:54 the data source to what was past in if it's there, right. 5:59 Now do you see anything wrong with this code, maybe we need to be more careful. 6:04 So if something's passed in and args.length isn't equal to 2, 6:08 they didn't pass both the parameters. 6:13 We need them to pass it, otherwise we'll blow up here. 6:16 So we'll do a, we'll tell them how to use it better, right, usage instructions. 6:18 Let's say java Api, and we're expecting a port, and we're expecting a data source. 6:23 And this is just kinda what we'll print out 6:28 on the command line if they try to press something there and let's exit. 6:31 Why keep going if we haven't passed in two arguments. 6:35 Cool, so now we'll use some string formatting here to our benefit. 6:40 We'll say String.format and we'll put back in that data source, right? 6:44 Boy, there's a lot of quotes going on in here, isn't there? 6:51 Always a little bit dangerous there, so we're gonna pass on the datasource and 6:54 then finish that off. 6:58 There we go, that looks good. 7:00 Okay, we're getting so close. 7:07 So if we flip back over to the ApiTest, and 7:09 we'll jump back into our before method. 7:11 Now remember that we need to keep the connection open or 7:13 the database will be erased each time. 7:16 We don't wanna do that. 7:18 So, what we should do is we should make a new connection and 7:19 open it up and keep it open for each test. 7:21 And then therefore when the test is over, the database will get erased. 7:24 It'll work kinda like magic, right? 7:27 So we'll say, Sql2o sql2o = new Sql2o. 7:28 Hey look we get to use our TEST_DATASOURCE there. 7:36 And we want to also make sure that we do that INIT again. 7:41 Probably should put this in function someplace right? 7:48 I feel like I've written it enough, RUNSCRIPT from, 7:50 we wanna remember both, classpath:db/init.sql. 7:54 And we wanna remember that final trailing quote there, awesome. 7:59 And then we're gonna open up a connection that we can close, sql2o.open. 8:05 This is going to be a private field. 8:16 That is fine. 8:21 All right, and then we want to close it after each test. 8:24 So remember the before class is running before all the tests run, and 8:28 this is before and after are running, before each test and after each test. 8:31 Awesome, okay so it's about time to craft our own objects to manage calling our API. 8:36 Remember, Spark does not come with this out of the box yet, maybe it will. 8:44 So we'll need a way to test requests and responses. 8:48 So why don't we start with the response first? 8:52 I think that's probably the most clear one that we can do. 8:54 Now we can add files in our testing bit over here right. 8:57 So this is the main but 9:00 if we come down here in the testing we can add a new class in here. 9:01 So let's do that, let's do com.teamtreehouse.testing.ApiResponse, 9:05 so this will be the response that comes back. 9:11 And really all that we care about is, we want to know about the status, right? 9:16 And we want to know about the body of it, what does the body from the response say. 9:22 Don't really care about too many more headers do we. 9:30 So, let's go ahead and 9:33 generate a constructor, both of those, 9:36 and let's generate some getters for 9:41 both of those, there we go. 9:46 You know what, these shouldn't change ever so let's not let anybody 9:50 mess with the response, so we'll make these final as well. 9:53 Okay, so after they're initialized they can't change, cool. 9:57 All right, and now, we're at one of those places where there is quite a substantial 10:01 amount of finicky code to write. 10:06 It's a bit out of scope, but I want you to feel like you can do this. 10:08 There's nothing magic here. 10:11 I just kind of don't want to have you watch me type. 10:12 So let's do this, in the teacher's notes, there is a link to some code, 10:14 and you should go grab it. 10:18 And what we're gonna do is we'll make a new file right in here. 10:19 We'll make a new Java class and we're gonna call it ApiClient. 10:23 And from here I want you to just copy the code and 10:29 paste it in and we'll walk through it. 10:34 All right, so I've got it copied and I'm gonna paste it. 10:36 Okay, so let's walk this code. 10:39 So when we're creating our server, what you do is you pass it in. 10:42 And that's gonna be like our local host and our port. 10:45 We'll do that when we create it up. 10:47 And this is an overloaded method because we are not pulling in the body. 10:49 I'm gonna close this so we have more. 10:54 So we're not, you don't always wanna push in the body, right. 10:56 The body is only for posts, usually. 11:00 So this request here is the chunky bit of code. 11:04 So here we go. 11:06 So URL comes form the Java.io package, and here we're just going to build it, right? 11:07 We're gonna build the URL. 11:12 We're gonna concatenate the server that we know, and 11:13 we're gonna take the URI that was passed in here. 11:16 So that's /courses, right? 11:19 And we're gonna open up a new connection, okay? 11:22 And the way that connections work is you open them and then you set things on them, 11:27 and then you call connect. 11:31 So, we are going to open from the URL and create a new connection. 11:34 We're gonna say that it's a post or a get. 11:39 We're gonna tell it that no matter what we're setting this header here, 11:40 we're setting that the content type is application json. 11:44 And then if we have passed in the request body, 11:47 we need to let the connection know that it needs to do output. 11:50 It's a little wacky what you're doing here. 11:56 So then, what we do is we open up a connection stream and we're 11:57 going to put it in the try with block there, so it will close when it's done. 12:02 And we write out to it, just in case there's any unicode characters, 12:05 we want to write out of all the bytes there, so 12:09 that we can get umlauts and axon and all those things. 12:11 This line here actually makes the connection happen. 12:17 And then what happens is we'll get back either an input stream or an error stream. 12:20 So remember this is using a ternary operator. 12:24 So it's saying if this is true, 12:27 return the input stream, otherwise return an error stream. 12:29 And what this is here is saying anything with a status code 12:32 that is less than 400 is good to go. 12:36 That's a good thing. 12:39 Anything above 400 is an error. 12:40 Remember that from HTTP? 12:42 IOUtils here comes from Spark and 12:44 it's going to take our stream and turn it into the string that we need. 12:47 And then, of course, we're going to make one of those API Responses. 12:50 And return it back. 12:54 Whew! 12:56 And then if anything goes wrong, we'll kick up a RuntimeException so 12:57 that the test can catch it. 13:01 There we go. Let's see. 13:03 It looks like I forgot to paste the last trailing to close the client there. 13:06 Okay, so let me know what you think about this approach of 13:10 pasting versus slogging through writing this code. 13:14 But the good news is this is written. 13:17 Okay so we can use this now, right? 13:18 So why don't we make it available to all of our tests, right? 13:21 So we should put that in the before here, right? 13:24 So we'll go ahead and we'll say client = new_ApiClient and 13:26 we're gonna pass in http://localhost, 13:33 and once again we're gonna use our constant. 13:39 Cool, so, client, we need to make a field for this guy. 13:44 Create a field client and we have an ApiClient that we can use. 13:49 Few, right, so let's go generate a test. 13:53 So, let's make sure that when we create a new course that it returns 13:57 the proper status. 14:02 So we'll make a new test and 14:03 we will say addingCoursesReturnsCreatedStatus. 14:06 And we saw in postman that this is working, but if we write a test for this, 14:13 we'll always know that it's working, right? 14:16 So writing JSON strings is a bit of a bummer, so 14:18 I typically like to make a map and then turn that into JSON using our buddy GSON. 14:22 So we're gonna make a new HashMap, And 14:31 that is going to have some tests, put the values in there. 14:35 So we will do Tests and, I forgot to do values.put and 14:41 we want to put those values in there. 14:46 And we will scroll up a bit here and we will say values.put. 14:52 And we'll put in url and we'll give it our famous URL. 14:57 We start doing more of these, we can always move this around. 15:02 So now we have prepared, arranged our values, and we're gonna act. 15:05 We're gonna say API response, and 15:11 we'll get back a response object, and we'll say client.request, 15:15 and this is the thing that we just wrote, we want to post that's method. 15:19 The URI is courses, and 15:23 the body is going to be a gson.toJson(values), 15:26 and of course it doesn't know what gson is because we haven't talked about that yet. 15:31 So let's go ahead and we will say gson- new Gson();. 15:36 So it is a JSON and we need to make a field for that. 15:44 There we go. 15:50 We will now check that 201 is returned from our status. 15:58 Here we go, what do you think, is it going to work? 16:12 Let's make sure. 16:15 So we will switch to All in course-reviews, 16:17 this is being a test for that. 16:19 And we will go run. 16:21 And our API test fails. 16:25 No initial, need to specify a class name or system property. 16:28 I think I forgot. 16:35 And this Sql2o, I forgot the username and password. 16:39 That is such a confusing API that it doesn't, I always forget that. 16:42 I apologize. 16:46 That's the error that happens when you do that, just so you know. 16:46 I have now done that twice. 16:50 Sorry about that. 16:52 Learn from your mistakes. 16:53 There we go. 16:56 So here it is. 16:57 It worked. 16:58 Awesome. 16:59 So what else can we test now that we have a valid test? 17:01 So we need to get a course in place. 17:04 So let's go ahead and 17:05 let's copy our course from the other test that we have in place. 17:07 Pull up that new course. 17:12 So, over here, in courses, if we come in the DAO, and 17:16 we come in this DAO test, we will copy this new course method. 17:20 Now, if we end up doing this too much more, 17:28 we might wanna think about putting this elsewhere. 17:30 So, we'll add a new, and that creates a course. 17:32 And let's add a new test now. 17:37 So we're gonna test that courses can be accessed by ID, okay? 17:38 So do a new test method and we'll say 17:44 just like we said coursesCanBeAccessedById. 17:49 All right, so we'll make Course course = newTestCourse(); and 18:00 we will add it, courseDao.add(course). 18:08 And we need to make a new courseDao, don't we? 18:17 So we will do courseDao = new Sql2oCourseDao. 18:22 And it takes a sql2o. 18:31 There we go, and we'll make this a field. 18:34 Cool. 18:42 So now we have an added course in our test, and let's go hit and 18:43 make sure that we can get it. 18:47 So we'll do an ApiResponse res = _client.request. 18:49 We're gonna do a GET this time and 18:55 we are going to get back /courses/. 19:00 Let's do this on the next line "/courses/" +_course.getId. 19:05 So this is where you're using that method. 19:11 And we're going to pull back from gson, let it do the heavy lifting. 19:16 We're gonna retrieved back from the body there, or gson.fromJson. 19:21 So the body that is set in the response there, getBody, 19:28 should be able to populate our Course.class. 19:32 Then what we'll do when we assert, we will assert that 19:36 basically those are the same thing using the equality method. 19:41 So we're expecting the course and retrieved is what we're checking. 19:46 So we'll see if those are both the same. 19:49 There we go. 19:55 All right so we know that that worked too. 19:57 Awesome. So both of those are working, 20:02 so let's do one final test. 20:03 Let's see what happens when we say missing course 20:06 MissingCoursesReturnNotFoundStatus, right? 20:15 So remember that was the 404 thing we did. 20:21 So we'll just go ahead and get a response. 20:24 We'll act right away because we don't care if there's anything out there. 20:28 And we'll say request GET And 20:31 we will just look for courses 42. 20:37 And that should return a 404, we know that it doesn't. 20:40 But let's go ahead and we'll write a test that proves that it doesn't. 20:46 And then once we fix it, it will always work. 20:51 Okay, so let's run, we got a red error. 20:53 Expected 404 but got 200. 20:59 Let's fix that. 21:01 >> Now that we got our testing all set up, 21:02 we can start to feel better about the validity of our API. 21:04 Now, I realize that was intense. 21:08 And most frameworks have that sort of testing built in. 21:10 The good news here is, we are most of the way to writing a fairly generic way of 21:13 functionally testing Spark applications. 21:18 We could, in fact, extract that and refactor it into an open source project so 21:20 that others wouldn't have to go through that pain, 21:24 as well as they could extend and improve it. 21:26 Now another important thing to we test, 21:29 which we won't have time to get into here, is load testing. 21:30 Since you're publishing your API to the public and 21:33 anyone could be using it, you wanna know how much load it can take. 21:36 Spark does an amazing job of handling many connections at once, but 21:40 you really should see where it falls over and can no longer take care of requests. 21:44 As you saw, 21:48 we're getting super close to having a really nice API that we can iterate on. 21:49 I think we should really fix that failing test. 21:53 And make sure that when an error happens, 21:56 we send the correct error message and HTTP status code from our API. 21:57 Let's go turn that red to green. 22:03
You need to sign up for Treehouse in order to download course files.Sign up