Tip:
Highlight text to annotate it
X
So, moving on to TBDD for the model and stubbing out the Internet. Which we had a
preview last time we said we were going to do this. So now we're on this model method
that we've been stubbing out, find in TMDB. So what should it do? Well, we have
this introduction to the Ruby TMDB gem, which wraps the API for TMDB. Now if we
have no gem, what the model method would do is probably just submit a restful URI
directly to TMDB and do something with the results. We do have a gem, so in this
case, we'd just say well, the movie method should just call that gem and make sure
that it passes the right arguments to it. And what if the TMDB ruby gem signals that
something went wrong? Maybe the API key that you passed is an invalid key that
doesn't correspond to anybody or, or you fail to provide one. And this is the first
example I think we've seen of explicit versus implicit requirements. So the
explicit requirement on this model method is, well, it's gotta call TMDB, right. We
can't get around that. But the implicit requirement is wait a minute. When you
call TMDB. The docs for TMDB say that you have to pass a valid API key. And you, if
you look at the screen cast that's mentioned in the book, it turns out that
there's two different ways that could go wrong. One-way is if you don't give a key
at all, like, if you leave it blank. And a different way is if you give a key that
is, not blank, but it's not valid, right? So there's actually two different implicit
requirements that come out of the explicit requirement. The implicit requirements
aren't provided by the customer. But they're a direct consequence of doing the
thing that, that is sorta specified by the customer. So we can use two more keywords,
context and describe, to sort of group our, our spec tests into cases that cover
these two different contingencies. And again we're not gonna do a live demo but
I'll step through the different variations of what test conditions you might wanna
cover and I actually made some notes to make sure I don't forget anything
important. So, there's a couple of. Like three new things I guess in this example
that we're gonna look at, none of which are particularly rocket science but
they're useful to know. So before we just had describe to collect the group of
tested or about the same behavior or related behaviors, there's further ways we
can subdivide it using context. So remember we said we're gonna describe
searching TMDB but there is a context where we have a valid key and there's a
context where we have an invalid key and in, and the invalid key context has these
two different cases, one of them where the key is not given and one of them where the
key is given but it's a bad key. Now, why is there context as well as described?
It's for the same reason that cucumber has given when, then and but, but they're all
synonyms. Context and describe are synonyms, the only reason to use one over
the other is whichever one makes the test more readable. So this says, I'm
describing. Test related to the single collection of behaviors, but the same
behavior can occur in two different contexts. It?s purely for human
convenience to have the two synonyms. So, let's see, what was the other, stubbing.
Here we go, so let's take a look for a moment at the strategy that we're using to
test these two different cases of having an invalid key, because it's blank versus
invalid key because it just doesn't correspond to a key that anybody has. So
looking at the blank one. What are we doing for the blank case? We're stubbing
the API key method in the movie class. And if you look in the book you'll see that
the, we developed a simple method that just returns the API key so that so that
you can pass it down to TMVB. We're gonna stub the method that usually returns that
key and force it to return a blank key instead. Okay, well, you can see why that
is a reasonable thing to do if the goal is to test what happens when the key is
blank. But, looking down here, what happens if we want to test that the key is
bad? So it's not blank, but it isn't anybody's actual key. Wait a minute. Here,
we're not stubbing movie, we're stubbing the gem. [inaudible] movie. And we're
gonna stub it's find method, which is the thing that calls, the thing that calls the
real TNVB site and we're gonna cause it to raise something. So, whereas before we
said we're gonna stub and return a particular value, here's a case where
we're gonna stub a method and make it raise an exception. So what's going on
here? Why are these two stubs stubbing at different places. And the reason for this
is when we discussed, in the book when we discussed the behavior of TNBB and the
ruby gem. And there's actually a screen cast that shows this in action, is that in
fact the two failure modes are very different, right. So we're using this
underlying library, TNBB movie, but it does two completely different things for
these two failure modes. If you give it a blank key, TNBB movie knows immediately
that the blank key cannot possibly be valid, so it doesn't even try to call
TNBB. It just immediately raises an error. So, and in fact in the screen cast we
watched it do that. And, what we're doing is we're exactly mimicking, right, we're
creating effectively a stub that mimics the real behavior that we saw so we can
test in realistic conditions. In contrast, if you pass the TMDB ruby library an
invalid key, well, there's no way the library can tell if the key is invalid,
right? It has to actually call TMDB, and then TMDB will say, whoa, whoa! This key
is not blank, but I don't recognize it, right? So that's why there's two different
failure modes. The easy failure mode is, we know a TMDB, the ruby TMDB [inaudible]
is gonna barf on this one, so that's fine. We just, we just used this razor, to
simply simulate that barfage, which is a real word. On the contrary, in this case,
we know that TMDB movie is gonna try to call TMDB. We need to prevent it from
actually doing that, right? We don't want it to be the case that every time we run
our tests, it will really and truly call TMDB with a known bad key. So we'll step
out of the place where it tries to make that call, and we will raise the same
exception that we watched it raise when we did this manually, right? So this, it's
kinda, it's a very similar concept. There's a piece of code that we wanna
break a dependency on. We observe how that piece of code behaves. And then we use
seams to simulate that behavior in a controlled manner in our test cases. One
of the things to notice about this, which I think is pretty interesting, is, Let's
just pick one of these at random, cuz we can illustrate with either one. [cough]
[inaudible]. Sure, here we go. [inaudible] movie stub, find, and raise a run time
error. Okay, so like I said, instead of saying we're gonna stub the method and
force it to return something, we're gonna stub the method and cause it to
[inaudible] an exception. The question is, how do you test that that's really what's
happening. And that's where this lambda comes in. Right. Basically what's
happening here is; we're going to take, we're going to make a little procedure
that calls find tmbd and we're gonna wrap that procedure in a lambda expression
saying this procedure when called should raise an error. Alright, and this is a
subtle thing but it's important to realize what would happen if we took out the
lambda. If we had just, if we took out. This part. Right? And we just tried to
call move find and TMVB what's gonna happen. Well we stubbed find and TMVB
yeah, well, findinTMDB is going to call TMDBmovie. We stubbed TMDBmovie and are
going to force it to raise an exception, so this highlighted statement is actually
going to raise an exception. That's bad, right? If a test really raises an
exception, the game is over, the test has failed. Tests should not raise exceptions.
However, we want to check because in this case, the correct behavior is for an
exception to be raised. We need to raise it in a controlled environment. And that's
what this lambda construct allows us to do. This is just a regular part of Ruby
and it executes this procedure in its own closure and lets should inspect the
output. Right. So, in fact, if this expression tried to fit on an exception
the exception would be captured as part of the lambda closure and now should can take
a look at and say, yes. It actually tried to raise this particular exception. Right
so that's what's going on with lambda there. Other than the use of lambda, this
actually is just using constructs that you've already seen before. So. Let's kind
of relate this back to our big picture of testing. [sound] we have implicit
requirements that are derived from the explicit ones. In this case it was by
reading the documentation, of the Ruby TMTV Jam by actually watching it interact.
When we, when we manually use it to interact with TMDB we see what happens.
And then we use seams, to break those dependencies and simulate the behaviors in
a controlled manner. And in this case, we have to do it in two different approaches,
right? In one case, because we previously observed how TMDB works we know that with
a blank key, we can just immediately you know, we can catch that error, make sure
that we convert it to the right exception type. But in the other case, we needed to
prevent the TMDB gem from even trying to call TMDB. We new that it would try, and
we knew that it would raise an error when it tried. That's the thing that we wanted
to actually do in a controlled manner. And we saw that you can use context and
describe, which are actually synonyms, to group similar tests and if you read a
little bit in the book it talks about using before so that if you have a group
of tests that are actually related you can have a, a common set of behaviors that are
executed before all of those tests and that helps you [inaudible] them out,
somewhat. So, this kinda raises a more general question. Which is pretty
important in a service oriented architecture point of view. Which is,
given that the point of stubbing is to control behaviors and break a dependency
from your test, where is the right place to do it? So, one example that we just
saw, is for a blank key we stubbed right at the level of movie. Right, so from the
point of view of the class under test. Everything below this point; below this
dotted line was being stubbed away, right. We, we got rid of all of that stuff, and
we faked its behavior in a controlled manner. On the other hand, we also saw
that for one of these cases that wasn't enough, right? If we pass, an invalid key
to Ruby TMDB, there's actually no way for it to know in advance that the key is
invalid. It's actually gonna try to make the call to the TMDB website to figure
that out, and that's the part that we wanna prevent. Right so, we can't test
that case by stubbing at a higher level. But we can test it by preventing the call
from happening at all right. We can stub its find method. And how do we do that?
Well we look at the TMBD documentation, and it says the find method will call TMDB
and do some stuff. Okay well that's what we wanna stub out, no problem. Ruby lets
you create the stub anywhere you darn well please. What if the TMDB API was actually
less well documented? We didn't even know. Well okay there's 28 different methods in
there. Which is the one that's actually gonna try to call, the TMDB website? And
how would we [inaudible] it if we don't know which method it is? Okay no problem.
Fake web. Right, we can stub the entire HTTP client library in Ruby. And there's
actually a gem is called fake web that does this. It doesn't work exactly this
way, but conceptually this is how it works. You basically say, hey if anybody
tries to fetch from this URI, Here's how you should react. Or if somebody tries to
fetch from, you know, a URI matching this wildcard pattern, here's how you should
react. And although it's not implemented in this exact way, you've already seen the
concepts that underlie its operation. It uses stub, it uses with to be selective
about different arguments that you pass could cause different behaviors. And it
uses and return, to force the return value to something you like. And hopefully later
we'll have an assignment on doing Facebook Connect and we'll basically be testing it
like this. What if that is not good enough? What if you've got a crazy library
that doesn't use the standard HTTP support in Rube, but talks straight to the OS?
Well, some services are nice enough that if you are a developer, they give you
access to the secondary site that is sort of a copy of the main site. But it only
parses requests that are, are contrived in a certain way. So they basically. It's
like providing a server-side stub. They basically say: Here's a test server.
Here's a table of 25 different values that you can pass to the test server, and how
the test server will respond. And the idea is that they will let you simulate, in a
controlled manner, whether your app would respond correctly when the real server
misbehaves. So, for example, if you do any PayPal development, PayPal does this,
right? They have a sandbox which has the exact same API as the real PayPal, but it
only reacts to a certain subset of requests. And they have a documentation
table saying, here's how you use it for testing. So the idea is, when you've done
everything you can up at this level, the next step is, you try to do stuff down at
this level, talking to the real server. But it's not really the real server. It's
the real fake server, or maybe it's the fake real server. And we talk about the
employment and operations later in the course we'll see many, many more examples
of things like this. So, kind of, what's the moral of the story here? For unit
testing you're trying stub nearby because your goal is maximalization. Right? If
we're testing the movie class then, if the movie class does something that goes to
this stack. The closer to movie we can stub it the better because we are trying
to isolate it from it's collaborators and we also wanna keep it fast and independent
of all the, these other pieces. On the other hand, if you're doing integration
testing, for example you're running the user story in Cucumber, that tests this
behavior, you want to step as far away as you can, without breaking independence
because the goal is, for integration test, you're testing the interfaces. So for
Cucumber, probably stubbing somewhere in here would be the polite thing to do. Now
you could say, now wait a minute, integration test? You might as well just
stub all the way to the fake server, right? If they have a development server,
why not use that? And yeah, you can do that, but if you're running thousands of
integration tests a day, it's like not nice to hammer their development server.
So the idea is, you do as much testing as you can up here and when you're sort of
ready to go live, the last set of sanity checks is you run a set of integration
tests down here and we use [inaudible] labs to do that later in the semester, if
we can. So, the moral of the story for stubbing the Internet, is, first of all,
there's almost always more than one way to do it. And the right place to do it
depends on, what's the focus of the test? Are you worried about whether the model is
working right? So, actually, everything below the model is kind of irrelevant. You
wanna make sure that the model can deal with an external error. If so, stub the
thing the model depends on, and force that error to occur. You can create your own
test stubs, like we did for the TMDB site. You could actually manually exercise the
site, read the API documentation, see what the interesting behaviors are, and then
use the different kinds of seams provided in our spec to reproduce those behaviors
in a sort of controlled environment. And by the way as an aside, this is how the
auto grader is tested. Right, the way the auto grader works is for complicated
reasons, you can't run R spec from inside and R spec test, as I learned [laugh]. But
you can run it from Cucumber. So there's actually Cucumber scenarios that say, if
the student submits this code, this is what the out, output of the auto grader
should be and we actually stub out all of R spec to simulate how your homework gets
grader. Isn't that cool? I think it's cool. Okay. Lets kind of augment our
testing technique slide, and I think we'll be, done for the day. So some new stuff
that we've seen. One of the things in the previous example, that we, we glossed over
a little bit is other things that you can pass to with. We've seen, should receive
with a particular argument, but suppose the argument is a hash, and all you care
about is that the hash includes a particular key value pair. You could say
that, hash including. We saw that in addition to stub and return, you can say
something like stub and raise, if you want to test that a method is raising a
particular exception when you call it under some conditions. We saw that you can
use describe and context to group similar tests together and that whenever you have
a describer context, you can associate a before block that applies to everything in
that, within that context. And that helps to dry out some of your preconditions. And
thankfully in this last segment, we didn't use any new Rail specific things, so, you
know, your brain gets to rest on that.