BRIAN YU: OK, welcome back everyone, Web Programming with Python and JavaScript.
And we'll pick up where we left off last week,
which was exploring Django, which was a new Python web framework that we were
able to use to continue to design more sophisticated, more interesting, more
advanced web applications and do so more easily.
It gave us access to things like a model already
in place for how to authenticate users, how
to log them in, how to log them out, how to remember that a user is logged in.
It also gave us access to a really nice and easy to use
administrative interface which made it such
that if we wanted to very easily add new things to our models
it was very easy to do that.
And it also gave us the ability to enforce relationships
between different models in ways that was easier
than we would have had to have done previously, whereby if we wanted
a relationship, for example, between flights and individual passengers,
rather than have to create for ourselves another table that would relate
an individual flight ID to an individual passenger ID
we were able to leverage Django in order to simply say
that we want there to be some sort of many to many relationship
between flights and passengers.
And taking advantage of that relationship,
we could then use that in order to have Django do the hard work for us.
So figuring out that we need some sort of intermediary table,
and Django goes ahead and creates that.
Well, we don't need to worry about how exactly Django's doing it.
We just need to worry about what relationship we want.
And so before we dive into the main focus of today,
which is going to be about testing and also
continuous integration and continuous delivery,
we're going to talk a little bit more about Django and the features
that it gives us to touch on a couple of things
that we didn't quite get to last time in order to answer a couple of questions
that people have had.
And so the first thing we'll take a look at
is inside of the same airline example that we've been using,
we'll take a look at how we might customize the administrative interface.
And so a couple of questions were raised during the last lecture about
whether or not we could change anything about the administrative interface,
that thing that's automatically created as one of the built in Django
applications.
And in fact there are things that we can do in order
to customize it to our liking.
There are in fact, many different options that we have available to us.
But I'm just going to show you a couple just to give you
a sense for what's possible.
So if I look go into Admin.py, which is the place where, in last lecture,
we really just had these three lines at the bottom where
we registered our airport model as something that we should
be able to edit in the admin interface in addition
to the flight and passenger model as other things
that we wanted to be able to edit and modify in the admin interface.
What we can do is extend the existing model admin class
in order to add additional features that we might want.
And the full list of all of these features
is available in Django's documentation.
But I'm just going to show you a couple that might be interesting or of use.
So one, for example, is that you can determine
how users are able to interact with particular data points.
So for instance, I'm extending the model admin class
to create a new class called passenger admin.
And this is going to be a special set of configuration settings
that I'm only going to use when I'm editing passengers.
And I use that down here when I register the passenger class for the admin site.
I'm saying, use this special passenger admin class
which is going to contain additional settings
or additional information about how I want the user of the admin interface
to be able to interact with passengers.
And in this case, I'm using this special variable called filter horizontal
which is going to give me a really nice way of taking a passenger
and manipulating what flights they're a part of.
So I'll show you what that looks like.
Filter horizontal is one of those built in settings
that I can manipulate using Django.
And if I run the server now, and go to this URL,
and I go to /admin to get to the admin interface.
What I can do is, now when I go into passengers
and I click on an individual passenger like Alice, what
I get is, this is what the filter horizontal administrative window looks
like.
And I'll get a really easy way of controlling what flights
that they're on.
Before, if you recall from last week, I was clicking
and there was Control or Command clicking on different flights
to either select or de-select them.
Now I can determine what flight Alice is on just by clicking on a flight,
moving them back and forth.
Now Alice is not on any of the flights.
If I click on another flight and move it over,
now Alice is on both of these flights.
And so these are just small changes I can
make to change the appearance of individual flights.
And one other thing you might have noticed from last time.
And before I do that, let me go back to the way it was before.
If I were to go into Admin and go and edit an individual flight,
for instance--
I want to edit flight number one--
you'll notice that normally what I get are just the options
to edit the properties of that flight.
I can edit that flight's origin, I can ediit the flight's destination,
I can edit the flight's duration.
But I can't, built in here, edit the flight's individual passengers.
And why not?
In the passenger side I was able to edit the flights,
but I can't seem to do the reverse.
Well the reason, if we look into models.py here,
is that if we look at what properties the flight has we only
defined origin and destination and duration.
Whereas for passengers we defined their first name and their last name
and this additional property called flights
that was associated with passengers.
So therefore when we go into the passenger admin interface,
I can edit what flights that passenger is on.
But when I go into the admin interface for editing flights,
I can't control what passengers they're on.
And so this is something we can also solve just
by doing some customization of the administrative interface.
So what I'm going to do is first define a new class
which extends StackedInline, where StackedInline is just one
of Django's built in classes that lets us, in a stacked format,
add new relationships between objects.
And so I'm going to call this one PassengerInline,
and you'll see why in a moment, where this is going to be the class
that represents the place on the admin interface
where I would like to be able to add and modify passengers.
I need to specify what model I want to use,
what data I want to be associated with this class that I've just created.
And in this case I'm saying passenger.flights.through.
This additional .through property refers to that intermediary table.
So recall that we have a passengers table and a flights table,
but there is some sort of relationship table between them--
some table that connects individual passengers to individual flights.
And this .through is how I sort of access that intermediary.
And extra equals one just means that I want
to be able to add one additional passenger at a time.
And you'll see what that looks like in a moment.
And then in the special flight admin class
that I'm extending from the existing admin class,
I'm saying I want to add this additional inline section of the Admin page called
PassengerInline, which is the one that I created up there.
And so if I now register the flight model to my admin site,
specifying that I want to add these additional settings in the flight admin
settings, now when I go to edit an individual flight
in addition to getting to edit the origin, destination, and duration,
I also see this view, which is the stacked inline
view, where it's just a stacked list of all of the passengers on this flight.
Whereby if I want to add an additional passenger to the flight
I simply need to go down here and select which passenger that I want to add.
Of course, in this case, both passengers are already on the flight
so it wouldn't make much sense for me to add one of those passengers again.
But that would be the sort of thing that right now our application can't handle
but you might want to add additional constraints that help enforce.
And so the goal of this is just to show just a very brief teaser as to how
you might be able to manipulate or customize
the administrative interface to look and feel exactly the way
that you want it to do.
And if you take a look at the Django documentation for the admin interface,
there are a whole lot more settings than just the couple
that I've shown you here that you can get a taste for,
that you can experiment with.
And I would encourage you to experiment with it as you work on project three
just to get a sense for the different ways
that people can begin to interact with this administrative interface
that Django has really built out for you.
The other thing that I wanted to touch on last week
that we didn't quite get to was how to deal with static files.
So a couple of people have asked about how you use static files in Django,
like CSS files or JavaScript files that are external.
So the way that you would do that in Django is, inside of our HTML file,
so one of our Django templates, what we would first need to do at the top
is use this additional Django command called load static which
loads static files into this page.
And then if we wanted to reference a CSS style sheet, for example,
this is the specific Django syntax for doing that.
So it looks a little bit different than the way
it looked in Flask, for instance.
But inside of the curly braces and percent symbols,
we'd have the static keyword just to say we want a static file to be used here.
And then which static file, well, it'll be the flight's directory
and then styles.css.
And if you look at the structure of our application
here, inside of the flight's app, I have inside of it
not just a templates directory that has flights and then based
on HTML along with all these other HTML files,
but I also have a static directory, inside of which
is the flight's directory, inside of which
is this styles.css file, which is the static file
that I want to load in all of my HTML files.
The question also came up last week as to why
it is that we need this additional flights directory.
In the same way that we had an additional flights directory
inside of templates, we also have this additional flights directory inside
of the static directory which feels a little bit redundant because we already
have a flight's directory, which is the name of the entire app.
And the reason, again, is the same exact thing as before.
It has to do with the fact that if I had multiple different apps, each of which
had a styles.css file for instance, a pending flight slash in front of it
helps to namespace that file, to make sure that I know how
to reference it in order to access it.
So just wanted to show you those things just
to give you a sense for a couple of final things in Django
that are possible that you may be interested in doing
as you go about working on your project, though it may not actually, in the end,
come up.
But the main focus of what I wanted to talk about today
is ultimately going to be about testing.
And so as we begin to develop more and more sophisticated, more complex
web applications, it's going to be increasingly
important to test that code in order to verify that it's accurate.
And so reasons why it might be good to test
that code are to make sure that if I'm making changes to one part of the code
base, making changes to one function that might be used
in a dozen other places across the application,
I want to make sure that the changes I'm making to this one function
aren't going to cause some other part of the web application to break,
for instance.
And it might also be important because I'd
want to anticipate the fact that functions may behave differently
based on different types of inputs.
And so I want to account for the fact that no matter what sort of input
I provide, I want to know that my application is
going to be able to handle them well.
And so what we're going to spend a lot of time today focusing on
is how to build out a system for testing.
First of all, how you would even test things on your own.
But then looking at what tools and technologies exist in order to help
facilitate the testing process which ultimately becomes very, very important
as we begin to develop more complex web applications.
So to take a just very simple example, I wrote inside of--
let's go to the source and go to prime.py,
which is a function that I wrote.
Inside of a file called prime.py I wrote a function called is_prime.
And what this function does is, it determines whether or not
a number is prime.
And this has nothing to do with web applications just yet,
but I'm going to show you this example as a function
that I might want to test to see whether or not it's accurate.
And then we'll go from there in order to later
explore, how do we apply these same ideas of testing a function in command
line Python to going on to how do you test functions in a Django web
application, for instance.
So this function, is_prime, is going to take a number
n, a non-negative integer, and tell me whether or not it is in fact prime,
returning true if it's prime, returning false otherwise.
And the basic logical flow for this function
is that if the number is less than two, well,
it's not prime because two is the smallest prime number.
So we'll go ahead and return false.
And then I'll run a loop going from two up
until the square root of the number-- math.squareroot
is a function that gets me the square root.
And if it's the case that n mod i, where mod
is an operator that gives me the remainder when I divide n by i--
if I divide the number I'm looking at by this i, this variable in the loop,
and that number comes out to be zero-- in other words,
n is evenly divisible by i--
well that means that the num n is definitely not prime
because i is one of its factors.
And so I'm saying, return false.
The number is not prime.
And then finally, if I made it through checking all of these factors
up to the square root and none of them divided
n evenly, then I can say that this number is in fact prime.
And so at the end I'm going to return true.
And so this is the basic logic for the is_prime function
that I wanted to create.
And now my goal is going to be, how do I test this function?
How do I evaluate whether or not it's going to work or no?
So one thing I can do is to test it is just to test it on my own.
So I can open up the Python interpreter and say, from prime import
my is_prime function.
And now I have my is_prime function and I can test it.
I can say, OK, is_prime of 23, well 23 is a prime number
so this should return true.
It should be prime.
And it does in fact.
And likewise I can say, is_prime 28.
Well, 28 is not a prime number so this should be false.
And so I press Return and indeed it does turn out to be false.
And then I could try, is_prime 25, for instance.
Let's see, well, 25 is not prime either because 5 is one of its factors.
And OK, something seems to be wrong with my is_prime function.
My is_prime function thinks that 25 is in fact prime
when in reality it's not prime.
So I found some sort of bug or error inside of my is_prime function.
But all of this is ultimately going to be pretty tedious
because as I think about using this function in the broader
context of a larger web application where I'm constantly making changes
to different parts of the codebase where I might be changing
different aspects of the function, I don't want to have to,
every single time I make a change to the program
and I want to make sure that the is_prime function is working,
to go in here and type is_prime 23 and 28 and 25
and try a whole bunch of examples to make sure that it's going to work.
So what might I do instead?
What are some other things that I could do just to simplify this process
or make it so I don't need to manually go into the Python interpreter
every time and try out the is_prime function
on a bunch of different inputs?
AUDIENCE: [INAUDIBLE].
BRIAN YU: Great, exactly.
I can write some sort of program that's just
going to run a whole bunch of these for me
in order to make sure that I have the right idea.
And so let's try doing something like that.
If we open up testzero.py, what I have in testzero.py is a very simple Python
application, or really just a Python module that
contains a single function called test prime,
that is going to test whether or not I get the expected value that I want.
So if I feed into this test prime function a number n
and a Boolean value of what I expect the result to be,
this expected value, then I can check if--
so run the is_prime function on n, because I imported it
up here at the top.
And if I check the is_prime function and it's not equal to the expected value--
in other words, when I perform the computation it wasn't what I expected
it to be based on what I thought the test should result--
then there was some sort of error.
There's some sort of error in my is_prime function.
And so I'll print something like, there is
an error on is_prime n, where n is whatever the number.
We expected it to be whatever we expected it to be.
And so this function is going to simplify
the process of allowing me to test whether or not a number is prime.
And so in order to use this, now I just need to call the test prime function.
So I could write a Python file, for instance,
to just use the test prime function over and over again
in order to test whether a number is prime.
Another thing you'll sometimes see is people writing just a bash script,
just a script that's going to run inside of the terminal shell without anything
more.
And so the way bash scripts work is that they'll generally just
be a bunch of lines of commands that I might run on the command line.
And in this case, Python-C is just a way of saying, run this particular Python
command.
And so on line one I'm saying, run this Python command.
First import the test prime function.
And then test to make sure that one is not prime.
And likewise, I'm doing the same thing on this line saying,
test to make sure that two is prime, and make sure that eight is not prime
make sure 11 is prime.
So on and so forth, testing all of these possible input values.
And now if I wanted to run this I could run that script.
And what I get is for the two for which the number did not
match the expected result, where I ran is_prime eight,
I expected eight to not be prime but there seems to be some sort of error
here.
I get that there is some error that's listed here.
And so that's helpful.
Now, any time I make some sort of change to my web application or my application
in general, I can run a sample test file that's
going to run a whole bunch of these tests and then give back to me
some information about any line of output
that I get here is a sign that something wasn't quite right,
that something went wrong over the course of me trying to test
this function or this application.
So if we go back to prime.py, I can begin to start debugging this,
try and figure out what exactly went wrong.
Does anyone have a sense for what might be wrong, out of curiosity?
Why it seems to be thinking that more numbers are
in fact prime when they're not?
If you played around with it for long enough,
you might find that it has to do something with this loop.
That for some reason I'm not testing enough possible values in the loop.
And in particular, it has to do with the fact
that when Python does loops in a range, it
includes the first thing in the range but it
excludes the last thing in the range.
So for instance, if I do for i in range zero to five, print i, what I get is,
I start with zero and it goes all the way
up to for but it doesn't actually include five.
So it includes the first thing in the range
but it doesn't include the last thing in the range.
This is a common source of what we call off
by one errors in Python and other languages, where we had the right idea
but we were just off by one number in something
that we were trying to calculate.
And so the problem in this is prime function
just happens to be another one of those off by one errors
that really, in order to include that final number, that square root that I
want to test, I additionally need to test that last number,
the actual square root itself, or the number closest to it
if it doesn't have an integer square root.
And so now that I've fixed the function and I
want to test it, rather than go back into Python
and then saying from prime import is_prime
and then testing all of these individual values again, all I have to do
is run that test file again.
And this time the test file produced no output.
Remember output was only going to be produced
if there was some sort of error, if there was a line that was incorrect.
And now when I run the test file I get no errors which tells me that, at least
for the inputs that I provided, the test seemed to be working.
Now this doesn't mean that my function is necessarily totally correct
because it just means that whatever test I decided to give to this program,
it passed those individual tests.
But that's where it starts to become important to write
good comprehensive tests.
To make sure that if you're going to test
a function or a particular part of your application,
that you're really testing it under all possible conditions.
That we have tests for even numbers and odd numbers
and different types of numbers to really try and get a sense for,
is this function really correct by testing it on as many different things
as we can.
And that's going to be important as we start
to build larger web applications that are getting more complicated.
We want to make sure that our tests are testing
as many different possible situations that might come up as possible.
Questions on anything so far?
OK.
So we'll take a look at a couple other features of Python that
might be useful as we go about testing.
And one thing is Python's built in assert command.
And so the Python assert command really does one thing and one thing only.
It takes the expression that comes after the word assert and it asserts, or just
states, that this expression is going to be true.
And if it's not true, then Python will throw an error,
the same way Python throws an error if you try to divide by zero for instance,
or access something in a dictionary that doesn't exist.
It'll throw a particular exception known as--
it'll throw an assertion error.
And so if I assert like, the statement true, for instance, nothing happens.
It just states that it was true.
And it's not actually changing or modifying anything.
But if I assert something that's false, then I
get this assertion error exception.
The whole program quits and I get this error
that says the expression that came after the assert was not true
so there was some sort of problem.
And so this can be useful if I'm trying to see whether or not
a program is working correctly or not because if I want to state definitively
that something is supposed to be true, I can assert that it is a true.
And if it's not true, then my program is going to give me some sort of error.
And so if I look at assert0.py, for instance, here's
a very simple file that is just going to define a function called
square that takes a number x and returns x times x, returns the square of x.
And down here on line four, I'm just going to assert that the square of 10
is equal to 100, because that should be true.
The square root of 10 should be 100.
And I want to assert that that is in fact the case.
So now if I try to run assert0.py, nothing happens, right?
Assert doesn't print anything to the screen or have any other side effects.
It just asserts that the thing that comes after it is in fact true.
If it's not true, in an assertone.py for instance, if I try to assert a fact
that is not true.
I try to assert that the square of 10 is a 101, for instance, instead of 100.
Now when I try to run assert1.py, now I get this assertion error
because the thing that I was asserting was in fact false
and so the Python program is going to give me some error.
It's going to quit and say that something went wrong.
And in particular, all programs when I run them give me some sort of exit code
that indicate whether or not the program ended in a particular state or not.
And generally speaking an exit code of zero means the program ended
and everything went well.
And an exit code of something other than zero, like one or something else,
means that something went wrong as I was running this particular program.
And in bash, in the terminal environment that I'm using,
if I want to look at the exit code I can type echo $?.
And here I see that the exit code of this program was one.
Something went wrong and therefore this program
exited with a status code other than zero.
Whereas meanwhile if I had just run assert0.py,
and I looked at what the exit code of that was,
that exit code was zero because everything went fine.
There was no problem.
And so we'll return to this idea of exit codes
and how we can use the fact that exit code zero means everything's
OK and exit code one or something else means something
went wrong a little bit later.
But this just gives you the sense that we
can use these assertions to either have some sort of error
if something goes wrong, or do nothing if everything was OK.
But of course it's going to again be tedious
if we continually have to write these assert statements
after assert statements all the time in order
to verify that things are correct.
It would be nice if there were some easier ways to do this.
And Python does, in fact, allow us some tools
that make it easier to do testing.
And in particular, they have a framework called
unittest which is a Python library that's
designed to help make it easier to test your programs.
And so we'll take a look at unittest.
And then we'll look at how we can apply unittest to actual web applications.
And so what we're building up to now is in tests1.py.
And so this will look a little more sophisticated.
And what we've done here is, first, import unittest.
And now I'm defining an entire class.
This class is going to extend unittest.testcase.
So it's a class that's going to contain a whole bunch of individual tests
that I want to run on my code.
And each test is just going to be a function, or a method inside
of this class.
So I define a test called test1 and this docstring here, this comment,
is just a label for the test such that if the test fails,
then I'll know something about it.
I'll be presented with this label for it.
And so the name for this test, my description of it,
is that it's going to check that one is not prime.
And unittest has a whole bunch of these built
in functions for asserting things.
Rather than just a plain old assert that says assert some expression,
there is a special assert false, for instance,
that I want to assert false is_prime one.
In other words, assert the fact that if I evaluate is_prime of one,
that that should be false.
And likewise, in test2, where I want to check that two is prime,
I can likewise call is_prime applied to two
and assert that the result of that expression should be true.
And so I can define a whole bunch of these functions, each of which
is just going to be an individual test that I want to run.
So here is checking that eight is not prime, checking that 11 is prime,
checking that 25 is not prime, checking that 28 is not prime, for instance.
And then at the end down at the bottom I'm
just going to run unittest.main, which is going to run all of my unit tests.
In other words, run all of these individual functions.
And so if I now run tests1.py then what I get
is that it says it ran six tests in 0.001 seconds and, OK.
Everything was fine.
So nothing went wrong in this case.
I didn't get any errors.
If I change my prime function and put that error back in,
such that now the is_prime function is buggy, and now I try to run tests1.py,
now I get a whole bunch of these interesting errors.
So up here at the top it's telling me what happened.
This dot just means this first test passed, it passed, it passed.
This one failed, then it passed, and then this one failed.
And now it's going to tell me which tests specifically failed.
So I failed the test 25 check, which is the check that's
going to check that 25 is not prime.
Again, this was that description line that I included in that triple quoted
docstring.
And then it's going to tell me, all right,
what was the line that caused the assertion error?
Well the error with here where I was asserting the fact that 25 should not
be prime, that is_prime25 should evaluate to false
and it was an error because true is not false.
And likewise I got the same error on this test eight
function, where I wanted to make sure that eight is not prime.
And likewise I got another assertion error there as well.
And so that seems like a little bit of overkill for a function
this simple that's just testing whether a number is prime.
And it seems like we probably could have done this without using unittest,
and in fact, we did.
But as we begin to want to test more sophisticated, more complex
programs we'll see how the value of unittest really comes in
and how we can begin to build on this in order
to make our tests more comprehensive, make the process
of running tests even easier.
But questions about anything we've seen so far with just
unittest before we move on?
Yeah.
AUDIENCE: [INAUDIBLE].
BRIAN YU: Good question.
The echo command is just a Unix command that basically just
prints something out.
And in this case I wanted to print out to the screen
the return value, or the exit status, of the previous command that I ran.
Good question.
OK, so that was unittest in the context of just a basic Python file,
a Python module that contained a function.
Now let's try and apply this to unittest in the context, or unit testing
in the context of an actual web application.
And so let's go into airline1 for now.
And we'll take a look at inside of our flights application.
We've been looking at all these various files that we've had before.
So we had this admin.py file, which we saw
before helped us to create the admin interface however we liked.
The apps.py file was a file that just defined basic configuration settings.
Models.py was on we were using to define our different models,
whether they were airports or flights or passengers.
And we dealt with URLs in view last time too
in order to manipulate what people see and what URLs
they have to go in order to see it.
But the one file we really didn't touch last time was this tests.py file.
So Django comes built in with its own testing framework in order
to make it easy for us to run tests on our web applications
because it's designed with this idea that we should make sure
that our applications passing our tests before we deploy it for users to use.
And so if we open up test.py, here's some tests
that I've written in order to allow and facilitate
for the process of testing this web application.
And so the first thing you'll see up at the top
is that I'm importing from Django.test, I'm importing TestCase.
TestCase is an extension to the unittest framework.
Django has taken unittest and built onto it
in order to make it easier to test some Django web application specific things.
And you'll see examples of that later.
And so here's my class that's going to contain functions for each of the tests
that I want to run.
And inside of this class, the first thing that I do is set up the tests.
So this is a function set up, built into the TestCase framework, that is
going to run before any test ever runs.
So before any of my individual test functions
that we're about to see ever actually happen,
the first thing that's going to happen is
that this set up method is going to run which is going
to do a couple of interesting things.
And it's just going to set up the tests such
that I can test things that I care about later.
So maybe I care about testing things about airports and flights.
So in order to really test this, the first thing that I want to do
is set up those tests by creating some airports
and creating some flights inside of my database such that I can then
run some tests to make sure that they're behaving the way that they should
behave.
And so on lines 11 and 12 here, you see that I'm creating new airports.
So this is a slightly different syntax for creating objects
than we saw last time where before we just use airport and then all
of the properties and then we saved it.
This is another way of achieving the same thing.
Just wanted to show you both.
And so we can say airport.objects.create,
create a new airport with code AAA and city City_A.
Create another one with code BBB and city City_B.
And then create a whole bunch of these individual flights
where this has origin A1, A2, duration 100.
And a couple others have different origins and destinations
and different durations.
Now what am I actually going to test?
Well, let's take a look at an example of a function
that I might be interested in testing.
So I've added to models.py.
I've added to the flight model this additional function
called is_valid_flight.
So the issue came up during last week's lecture of, well,
right now based on the way that the flights are set up it's
not at all clear that a flight is necessarily a valid flight.
It might be the case that someone could create
a flight that goes from London Heathrow Airport to the same airport
and that wouldn't really be a valid flight
but we'd be allowed to do it based on the way that our database is set up.
And so maybe we want to add a function that is going to take a flight
and check to make sure that it is, in fact, valid.
And so that way when we display the page that
displays information about that flight, it
would also include information about whether that flight is valid or not.
And if it's not valid, then we could do something about it.
We might also, likewise you can imagine we
could extend this to have a page that would just show me
all of the currently invalid flights, all the flights that
don't satisfy some constraint that we want to have on flights.
And so what does my is_valid_flight function do right now?
Well right now it returns a Boolean value, true or false,
indicating whether or not a flight is valid.
And what does it mean for a flight to be valid?
Well in this case I'm saying a flight is valid if the origin of the flight
is not equal to the destination.
In other words, it starts somewhere and ends somewhere else.
If it's starting and ending in the same place that's not a valid flight.
And likewise, the duration needs to be greater than or equal to zero.
I don't want a negative length flight because that
wouldn't make a whole lot of sense.
Questions about is_valid_flight?
OK, so now what I might want to do is actually test this,
make sure the is_valid_flight method is actually
going to work because if I'm going to use it inside of my view, for instance,
then it better work.
And in fact I do use it in my view because if I
take a look at the flight.html page, this
was the page that before showed me just information about the flight
number, origin, and destination.
I've added an additional list item here that's just indicating whether or not
the flight is valid.
So valid is just going to be true or false by accessing
the is_valid_flight property of the flight, which will now
tell me whether the flight is valid.
And so you'll notice that if I run the Django server now and go here,
and I click on an individual flight, then what
I get is this additional property on the flight page
that's says valid true, meaning this slide is valid.
It would be valid false if it were not valid.
And so now that this valid function is something
that's integrated into the logic of my web application
I want to test to make sure that it is, in fact, going to work.
And so I've created two airports, airport1 and airport2.
This flight goes from airport1 to airport2.
This flight goes from airport1 to airpor1,
which would be a sign that that should be an invalid flight that I
might be interested in testing.
And here's one that goes from airport1 to airport2,
but it has a negative duration, a duration of negative 100 for instance.
And so now here's an example of a sample test.
I'm testing the departure count of an airport.
And so remember that last week when we set up our models for airports,
we also allowed for an individual flight to be related to its two departures
such that I can take an airport and access all of the departures
from that particular airport.
And so here what I'm testing is I'm making sure
that that departure's relationship really works.
So I'm getting an airport by saying, get me the airport that has code AAA.
And now I want to assert that these things are equal to each other.
I want to assert that the count of the number of departures should be three.
And so if the count of the number of departures from A1 is three,
then all is well and my test will pass.
And if something's wrong there, if the departure count is not three,
then this test will fail.
Let's see the other tests that I've written.
Here is one to test the arrival count in the same way.
There's only one flight that lands at City_A,
and so I'm testing to make sure that if I
count the number of arrivals at City_A, that should only be one as well.
And now here I'm starting to test whether or not
these flights are actually valid.
So let me try and get the airport1, airport2 in the flight.
And I want to make sure that this is the flight that
goes from airport1 to airport2 and has a duration of 100
because I might be trying to get some other flight.
And then I'm going to assert the fact that F,
this flight here, this should be a valid flight
because it's going from airport1 to airport2, it has a duration of 100.
That's a valid flight and I want to assert that that is in fact true.
And so I'm testing that.
Here, now let me test other things as well.
So in addition to testing whether a flight is valid
I want to test whether flights are correctly invalid.
And so if there's an invalid destination where
the destination is the same as the origin,
that should not be a valid flight.
So if I grab the flight whose origin is city one and whose destination is city
one, if I run F.is_valid_flight well, that should be false because that
flight should not be a valid flight.
And likewise I can do a test for an invalid flight duration,
that if I query for that flight that I created that has a duration of negative
100 and then assert that F.is_valid_flight is false, well,
that should work because this flight that has a duration of negative 100
minutes is very clearly not a valid flight.
And so when I run the is_valid_flight function then
that should result in false.
Questions about any of these tests or how they work?
Yep.
AUDIENCE: [INAUDIBLE].
BRIAN YU: Yep.
AUDIENCE: [INAUDIBLE]?
BRIAN YU: Good question.
What is the .get doing?
.get is just a simple way to query for a single thing.
So normally I might try and query something
by filtering to try and get only the things that are from CityA,
for instance.
And that might give me a query set of multiple different possible responses.
But if I only care about getting one thing,
if there's only one city that has city code AAA,
then I can use .get to just say, get me the thing that has city code A.
That only works, though, if there is a city with code AAA in the database.
And the reason that's there is because of this setup function
where I initially add a city here by creating
a new airport that has code AAA.
So if I were to do like .get equals CCC, for example,
that wouldn't work because that does not already exist in the database.
And so now that I've created these tests I
can begin to use Django's built in testing
infrastructure to actually run these tests.
And this is quite powerful for a number of reasons.
One, because it makes it very easy just to quickly run all of the tests
that I've created across all of my different applications.
But secondly because Django knows that my tests are likely
going to involve manipulations of the database.
They're going to involve me creating objects and testing them.
And it would be probably not a good idea if any time I ran these tests,
it would just actually run these commands on the database.
Why?
AUDIENCE: [INAUDIBLE].
BRIAN YU: Yeah.
It's going to mess up the data in a number of different ways.
And one, it's going to create these fake cities that aren't
actual airports inside my database.
But secondly if I ran the test once and then ran the test again, for instance,
it would run through this whole set up function again,
it would create a second city with code AAA and BBB.
And that's going to really mess up the data that I have in my database.
And so Django is smart enough to know that when we're running tests, go ahead
and don't use the original database.
Create a separate test database that we're
going to use purely for the function of running these tests.
And it's going to be blank and it will start from scratch.
And that will allow us to just create these objects
without needing to worry about what other data might already
be in the database, without needing to worry about messing up
the data that's already there.
And just allow us to really test the things
that we're interested in testing in a clean and isolated environment.
So how do we actually run those tests?
I'll go back here and I'll run Python manage.py and test.
And what you'll see here is that it's first
going to create that test database.
It's creating a brand new database.
It's going to be an empty database where I'll run all of these tests.
And then you'll see a very similar interface to what we saw on unittest.
It's really just running unittest on these tests because it gives me
these five dots indicating five tests all successful.
It ran those tests in 0.013 seconds.
Now it's OK.
Then it gets rid of the test database because presumably I no longer need it.
And now we're all set.
And so using that, I was able to very quickly,
by running Python manage.py test, run all of the tests
that I had previously created inside of this web application.
Questions about anything so far?
Yeah.
AUDIENCE: [INAUDIBLE].
BRIAN YU: Good question, yeah.
So we want this setup to only happen once before each test.
Each test, though, is operating in isolation to each other test such
that we never want it to be the case generally that what
happens while running one test might influence the running of a second test.
Because generally speaking, good tests are written independently,
where one test is not going to influence something else.
And if something goes wrong in one test it's
not going to affect the second test.
And so these are all operating independently.
And they all first set themselves up and then
run the test without ever interacting with each other.
And so this set up code will happen one time
for each one of these individual tests.
But the setup code for one test is not going to influence the setting up
of a second test, for instance.
So what we've done now so far is create tests that are testing the database,
really.
We're testing the back end side of our web application involving the models
that we have, the functions associated with those models,
and making sure that all of that is working correctly.
But that's not the only thing that we can
test because if we think about the relationships between different things
on our web application, if we think about it in Django's model view
template format where we have a model that has all the data,
our views.py file takes that information from the model and figures
out what to put into the template, and then the template displays
all the information that the user cares about.
Well, we've tested the model side of things.
But we really haven't yet tested the template side of things.
And so what we'd like to do now is test the template side of things.
Test not just the back end, but make sure that whatever information
we're passing into our template, that we're able to test that, too.
So let's take a look at that by looking at airline2.
So we'll take a look at test.py.
And in this case, notice we're doing a couple of things.
In addition to importing TestCase, we're also importing Client.
And what Client is going to do is simulate a web client,
a client that is able to make requests and get responses back from the server
such that we can simulate web requests to different pages
and make sure that the information we get back is what we expect it to be.
And so these first couple of tests are the same.
But let's now take a look at test_index.
And so what did the index function do, first of all?
Let's make sure we remember.
What our index function did, when we first go to the default flights page,
is that it sets up some context information
where we set flights equal to query for all of the flights.
And then we take that flights information
and we pass it into the index.html page.
That way the index page, very simply, would just
show a listing of all of the flights because of the context that we pass in,
this context variable here, includes within it
this flights key that's associated with a value that
represents all of the flights that we have inside of that context.
So how do I test to make sure the index function's working?
That if I had two flights in my database,
that when I pass it into the template I'm passing in both of those flights?
Well, here's the test_index function, which is just one of these tests
where the first thing I'm going to do is create a new web client--
I'm just going to call it C--
which is going to be able to make requests.
And now I'm going to do C.get, send a get request to the slash route,
that default route that should just load the default index page,
and get back the response.
And so a couple of things I want to do about that response.
First thing I want to do is make sure that response was successful.
Recall that when I make an HTTP request I get some sort of exit code
where generally 200 means everything was OK.
I want to first of all assert these two things are equal.
I want to make sure that response.status_code,
in other words the status code the came back from that web HTTP request,
is equal to 200.
That those two should be the same.
And if, for some reason I try to access the index page and there was an error
and I didn't get a 200 response, well then this test should fail.
And the second thing that I want to check
is I want to check the context of the response.
Remember the context was that object that I passed into the template.
And what Django allows us to do in order to make it easier to test our templates
is it gives us direct access to the context when
we're testing the response.
So if we want to make sure that inside the context that came back
there were two flights, because up above I created two flights,
then what I'll do is say, check the response context,
get at the flight's key, count them up and make sure
that there are two of them.
And so the reason this works is that if we compare it
to the views.py over here, in views.py I define this context Python dictionary.
And I pass that context dictionary into index.html.
Then when I run the test, I'm able to actually access that context
dictionary, get at the flights information in there, which is just
going to be one of those Django query sets,
count up the number of responses that came back from that,
and make sure that that number is equal to two
to verify that the index function is in fact working the way
that I expect it to.
So other things I can do here.
I can test to make sure that if I go to a valid flight,
then the flight page is going to work for me.
That if I try and access a flight it will work.
And if you recall, what does my flight function do in this views file?
I try to get the flight in question.
If it doesn't exist I raise a 404 error saying there was some sort of problem.
This flight number doesn't exist.
If I went to my URL/28 but there is no flight 28,
I'm going to get a 404 error saying, sorry the flight doesn't exist,
or at least I should.
And then I'll render this context, rendering information
about the flight, passengers on the flight, passengers that are not
on the flight in case I want to register them for the flight,
so on and so forth.
So if I'm now testing to make sure that I can get a valid flight,
well let me first grab a flight that goes from A1 to A1.
So this is actually not a valid flight but I
want to test to make sure that this flight page is going to work.
Then what I'll do is get a web client, try
to go to slash and then whatever this flight ID is, going to F.id,
saving that response.
And then, down here, asserting that the status code is equal to 200.
In other words, if I go to slash some ID, where
that ID is the ID of an actual flight, then this page should work.
I should get a 200 status code as a response from it.
On the other hand, what happens if I try and access
a flight ID that doesn't exist?
So here on line 60, I have an interesting query
which is a slightly more complex Django query
but I'll try to explain what's going on here.
I want to get the maximum value of any ID of any flight.
Because what I want to test in this function is,
I want to test what happens if I go to an ID number that doesn't exist.
If the IDs only go up to ID three or four, what happens if I go to /5?
So the first thing I need to know is, what
is the maximum ID of any of the flights in my database right now?
And so to do that I'm going to go to flight.objects.all, query
for all the possible flights.
And Aggregate is a special Django function
that helps to, as the name might suggest,
aggregate data by some particular attribute.
And in this case I'm aggregating it based on the maximum ID value.
So I'm going to get the maximum ID value of any of the individual flights.
And then when I actually do this test I'm
going to get slash whatever the maximum ID is plus one.
So I know that whatever route I'm trying to access now,
it's not going to be a route that exists because of the maximum ID was flight
number five, then when I access /6, as I'm doing here,
then there is no way that this should be a valid flight which means that
the status code that I get back as a response should be status code 404.
I should get that 404 error if I go to a flight that doesn't exist.
And so this function tests to make sure that is going to work.
And then you can see I also I have a couple of other tests here as well.
So here I'm testing to make sure that the passenger
page is going to work correctly.
So I first start by adding a passenger.
And then I check to make sure that when I go to slash this particular flight,
that if I count up the number of passengers
that I have that one passenger, for instance, on that flight.
And I also do the same sort of test to make sure that non-passengers work.
So if I have a new passenger but I don't register that passenger for a given
flight, then when I try to check the number of non-passengers
on this flight, that should be one.
And so what I'm doing here is by accessing the response context
and getting at particular aspects of the response context,
I can effectively test what information is
getting passed to my template, test what information actually
gets displayed to the user when they go to a particular page.
Which means I can now automatically run these sorts of tests
to make sure that my page is going to behave the way that it should behave
such that later on down the line if I'm making changes to the flight's page
and I change different things about what information is in the context.
And I want to make sure that this original functionality of displaying
the passengers, displaying the people that are not passengers,
is still working.
I can just use tests like this in order to make that happen.
Such that now if I go back and run Python manage.py test,
now I just ran all 10 of those tests.
Everything was OK.
And so now I can feel pretty confident that that is, in fact, working.
If I mess something up, though, for instance,
if in here rather than passengers being all the passengers and non-passengers
being only the passengers that are not on this flight excluding the people
that are on this flight.
If I accidentally switch them up, for instance,
and this was my non-passengers and this was my passengers, for instance.
And now I try to run that.
Well now I'm going to get the fact that I got two failed tests.
I missed a couple of assertions.
And here were the tests that I failed.
I failed the test flight page in non-passengers.
And I also failed the test flight page passenger's test.
And then I can go back and figure out why is it
that exactly I failed those tests?
I can fix those individual errors.
I can go back.
I can run the test again.
And now I can see that I passed those individual tests.
And so writing tests in advance and then running those
tests when you make changes is often a good way
to verify that your code is, in fact, working.
That you haven't made a change to one part of the code
and therefore messed up some other part of your application.
And that can be good for making sure the code is well integrated, that you're
avoiding bugs wherever possible.
Questions about things so far?
OK, so thus far we've seen a bunch of different ways to run tests.
We saw just running sample assertions.
We took a look at unittest.
And unittest has a bunch of other functions
that we can use in order to assert the things that are true are not true.
We saw assert true, assert false, we saw assert equal.
There's also assert not equal if two things are not the same,
assert that some item is in a list, for instance, or not
in a particular list, and a whole bunch of others that you can explore.
And so unittest is useful for quickly running
a whole bunch of functions that are each going to test
a particular aspect of our program.
And then we saw how Django was able to expand and build
upon those basic functions in order to allow us to test individual routes,
or test things about our database, or test the templates that
come back and looking at the context of the information in those templates
to make sure that those work too.
And I'll just show you one other example of testing in order
to give you a sense for the different types of testing that's available.
What about something like a JavaScript application?
So if I had a JavaScript application like counter.html,
which is a simple JavaScript application similar to things
we've seen before that just has a number and I
have an Add button that increases the number and a Minus button that
decreases the number where I can just press the plus button over
and over and over and the number goes up and up and up and up,
and then the minus button decreases that number.
How do I test to make sure that's accurate?
I can't just use like the Django framework
that I was using before, because it's not just a matter of request this page
and see what the response is.
Because the response will just be the number zero.
And I can't use that to say, test the plus and minus button
because that's not something about Django's server.
It's about something in the web browser.
It's about the JavaScript code that's really running in the web browser.
So how do I test that?
So there's an entire set of testing tools known as browser testing
tools that are designed for really testing things happening inside
of the web browser.
And one example of that is Selenium, which is the one that I'm
going to show you know.
And what Selenium does is it uses a web driver to allow
us to use Python code to effectively control
what's going on in a web browser, to pretend
to be the user programmatically, in order to manipulate
different things about the page.
And so how might that work?
Well, I'm going to go ahead and from Selenium import web driver.
And I'm also going to import from this test file a function called file URI.
What file URI is going to do is it just takes an HTML file
and turns it into the URL that I would actually
want to use if I were to open something like this up.
So for instance, if I take this URL and open it in Chrome, I get to the page.
This is just a file URL that now takes me
directly to the page that is located on my system.
And so that's all the file URI function is doing.
But I'll go ahead and save that inside of a variable called URI.
And now what I'm going to do is create a web driver by going webdriver.chrome.
So Selenium has a whole bunch of built in web drivers for different browsers.
Chrome just happens to be the one that I'm going to use now.
And what effectively this is going to allow me to do
is control Google Chrome through the usage of Selenium's Python API.
And so what I see is this Google Chrome window that now pops up that says,
Chrome is being controlled by automated test software.
I've installed something called Chrome driver inside of Chrome that
lets me now run these automated tests.
And so now if I say, well, URI is the file that I want to open.
So if I say driver.get URI, well that's going to, in here,
open up the page that I wanted it to open up.
And so now if I want a test, like I want to test
these individual buttons, the plus and minus buttons, how do I do that?
Well inside of counter.html, if I took a closer look at it, and I--
let's go ahead and open up.
Let's look at counter.HTML.
Inside the HTML I have these two buttons, Plus and Minus.
The Plus button has an ID of increase.
And so if I want to get at the Plus button,
well that's as simple as me now just saying, driver--
or lets me call the variable plus.
I'll say plus equals driver.find_element_by_ID increase.
So I want to get the thing that has ID called increase and save that as Plus.
And so now I can say, plus.click, for example.
I've extracted the element that has ID increase.
And now I can programmatically, in Python, using Selenium,
say plus.click to click on the plus button.
And ideally that should have had the result
of causing the number to increase.
It looks like that didn't quite work in this case.
What is-- plus is this element.
And I wanted to say plus.click.
No elements CLS.
So when I-- quick question, how does it know that it's a button?
When I'm extracting a web element, it knows information about the web element
that I'm attempting to extract.
So in this case it does know the fact that it is a button.
Let me give that another try.
So we'll go from Selenium, import webdriver,
and we'll say from tests import file_uri.
So URI equals file_uri of counter.html.
And now driver equals webdriver.chrome.
And now I have this Chrome web driver.
And I wanted to say driver.get this particular URI.
And now plus equals driver.find_element_by_id increase.
And now plus.click.
All right, not sure what happened last time but this seems to be working now.
So now I found the element that has ID increase, saved it
inside of a variable called plus.
And now I can actually manipulate that button
by doing something like plus.click, for instance.
And I do it again.
And that-- didn't seem to have the same effect.
So maybe there's some sort of bug in the web application that I can look for.
But the idea here is that we can begin to take advantage of the interface
that Selenium gives us in order to manipulate the page
and make sure that it's working the way that we want it to.
And so let me show you an example of those tests in action.
And so one thing I might want to do is test different aspects about the page.
So I might write a whole bunch of unit tests
where I first try to get the counter.html file
and then assert that the title of the page that I got is equal to counter,
make sure that the title is working.
And likewise I might test the increase button
by first getting the counter.html file, finding the increase button,
clicking on the increase button, and then making sure
that when I get the H1 tag, that heading at the top and check the text,
that the text should be one-- that that should have
the result of updating the counter.
And likewise, I would want to do the same thing for the decrease button--
make sure that when I click the decrease button,
it has the effect of subtracting one.
And then down below, I might test that the increase button works
multiple times, which seems like maybe it
didn't based on the opening of the file just
then, such that if I get the increase button and click on it three times,
then the result of that should be that if I check the contents of that
heading then that should be three.
And so what Selenium allows us to do is it
allows us to do some of this browser type testing
where I can really begin to test whether or not
things are working in the web browser in response to things
that users are actually doing.
That if they click on something and that has the effect of something
changing on the page, that's not something
that's very easily testable by Django's testing software, for instance.
But it is something that I can do by taking advantage of browser testing.
The goal of all that was to show you a bunch of different types of tests
that we can perform, whether it's testing functions
just by using asserts, using unittest, using Django's framework
to test databases and templates and contexts,
or using Selenium to test user interfaces
and testing the way that users actually interact with the web browser.
All of that is going to be very helpful as we
begin to build larger and more sophisticated web applications.
And what we'll see after the break is how we can actually
implement some of these tests and use some of these tests
in our own building and deployment process for web applications
as we take a look at CI and CD--
Continuous Integration and Continuous Delivery.
We'll take a look at that after the break.
We'll take a short break now.
OK, welcome back.
So where we left off was talking about testing
and how we can write tests in order to make sure
that different components of our application work
and then run those tests in order to make sure that anytime
we are making a change to one part of our web application
we're not inadvertently causing some other part of the web application
to stop working, such that if we're writing tests comprehensively
and thoroughly across all different parts of our application
it becomes pretty easy to make sure and verify the different components
and aspects of our application are working.
And so this is ultimately just part of good software development workflow.
And that's what we're going to be spending
some time with the rest of today talking about, which are just good software
development design practices and goals that we
should bear in mind as we go about designing software in the modern era.
And one thing you'll hear commonly when we deal with this topic
is CI/CD, which stands for Continuous Integration and Continuous Delivery.
So what does that actually mean?
Well, continuous integration has to do with consistently and frequently
integrating code together between different people
working on the same project, for instance.
Where in an old model you might imagine that once upon a time
there would be one central place where all the main code was.
And different developers working on the same project
might be working independently, separately on different features.
And then only at the end would they then try
to integrate their code all back together with the main original branch
in order to make sure that everything's working.
But that leads to a lot of different integration problems
where it's not necessarily immediately clear that when
you try to integrate all the things together
that it's just suddenly going to work.
And so what continuous integration is all about
is frequently merging different changes back
to some main branch that either you are working on your own project
or that multiple different people, if you're working in a group,
are working on together in order to make sure that changes that you make
are continuously integrated.
And using software version control tools like Git, this is actually quite easy.
And a second aspect to continuous integration,
which is sort of added onto this, is that any time a change is made
and we want to integrate it in with the rest of the codebase,
we'd like to verify that that change is actually going to work.
In other words, it's not going to break anything.
It's not going to cause any of the existing parts of the codebase to fail.
Because if you have one software developer who's
working on feature A in your web application,
they might not be thinking about features B, C, and D.
And when they integrate feature A into the branch, for all
they know it might be causing some problems to happen
in other parts of the web application.
So one important part of continuous integration
is going to be automated unit testing.
This idea that any time we make some change to the web application
and want to integrate it with the branch on which we're working on developing
this application, we automatically run some set of tests
and then we get some result. And any time I push code to GitHub,
for instance, I'd like to know whether that code I just pushed
actually passed the test or not by automatically running
those tests every time I push code so that I don't have to worry about doing
it myself and I might forget to do it, for instance, in order to really just
verify that this is going to work.
Continuous delivery, on the other hand, take this one step further.
In addition to just continually integrating code into the code base
consistently we also want to continuously deliver code
to our ultimate web application.
So as opposed to making a whole bunch of changes
and only after a couple of months of building up changes
to release of those changes in one go, continuous delivery
is all about making incremental deliveries to our web application.
That if we make one small modification and push that modification
and it passes all of those tests, then let's go ahead
and make it easy to then deploy that small change to our web applications
so people can see those changes right away.
And so these are growing in popularity as practices
when it comes to software development.
And we'll be looking at some of the tools that
help to make this a little bit easier today, which are tools that you might
see an industry quite frequently working on larger software development
projects like this.
And so there's many different CI tools for continuous integration
who are meant to serve the purpose of helping to quickly integrate code
and to run tests on code.
And you can explore a bunch of them.
They all do very similar things but have their slight differences
and advantages.
The one we're going to be focusing on in this class
is Travis, which just happens to be one of the more popular CI tools.
And the basic way that Travis is going to work--
Travis is the little guy in the lower right of the screen there--
is that when I push code to GitHub, when I've made changes to a web application
and I push that code to GitHub by pushing
some new change to my repository, what GitHub is going to do
is GitHub is going to notify Travis, our CI software, that we've
made some changes to our repository, that there has been
a new commit, a new push to the repository,
and to let Travis know that there has been this new change.
And what Travis is then going to do is it's
going to pull that code from GitHub and then run some tests on it.
Travis can do other things as well and we'll touch on that as well.
But one thing that it's quite good for is for running tests.
If Travis knows that there's been a change that's been pushed to GitHub,
and Travis can then say all right, let's pull
that new version of the code, the latest version of the repository,
run the tests, make sure that the tests are all passing.
And either way no matter what the results of the test
are let GitHub know the results such that on GitHub I can now see,
did the test pass?
Did the test fail?
And that can tell me something about whether or not
the change that I just made needs to be looked at again
or needs to be reworked, for instance.
And so before we actually look at specifics as to how that works,
questions about the general idea of what we're trying to achieve here?
Yeah.
AUDIENCE: [INAUDIBLE] into the branch [INAUDIBLE]??
BRIAN YU: Good question.
Question is about how you're merging and what branches we're merging into.
Different web applications will have different practices
for how this generally works.
But frequently there will be one branch on which the latest version
of the project being developed is on.
And as people work on individual features,
they'll work on those features individually
on different branches for each individual feature
such that if you make a new change to a feature that
gets pushed on to that feature branch.
And only once you're done with that feature,
or at a place where it can be merged in with the rest of the code
and it's working, then you'll want to merge that
back in with the rest of the code.
But what Travis can do is it can run these tests no matter
what branch you're on.
So even if you're not on the main branch, it can test your code,
make sure that it's going to work such that you can be confident
before you merge back in to the main branch on which all of the code is.
Because you want to know that when you merge it
in that those tests are going to pass, that you're not breaking anything
when you go about making that change.
OK, so how does Travis work?
How are we going to be able to take advantage and use this?
Well we're going to need some sort of configuration file
to tell Travis what to perform, what tests to run,
how to install all the things that need to be installed, for instance.
And to do that we're going to use a file format called YAML.
YAML is just a common file format that's used
for creating configuration files usually--
files that are configuring some service to behave in a particular way.
We'll see it for Travis now.
We'll see it again later on today in a different context.
But the general idea for YAML is that a file in YAML
is going to look something like this.
It's going to be a set of keys and values, just like in an JSON object,
for instance, which we saw before.
There were keys and values where we had key colon value.
Same general idea here--
key colon value.
And if we had a list of items, for instance
we had one key associated with multiple values,
it would look something like this, where a list is indented and has
a hyphen in front of it.
And so that would be what a YAML file would look like.
And what a Travis YAML file is going to look like,
configuration for Travis, our CI service,
is it's going to look something like this.
This is going to be the configuration file telling
Travis what to do in order to test our code to make sure
that it's working properly.
So we'll first specify what language we should be using, in this case Python
because our application is written in Python.
We specify what version of Python we want to run, 3.6.
We are going to specify what command or commands to run in order
to install the things necessary before we actually run our tests.
And so in this case, if we had all of our dependencies and requirements
stored inside of a file like requirements.txt,
for instance Django would be an example of a requirement
that we would need to install, we would include a line like pip
install -4 requirements.txt to say, go through all
the lines in requirements.txt and install all of those dependencies,
because Travis by default is not going to have Django installed, for instance.
So if we want to run our Django tests, then we
need to tell Travis that you need to install Django first
before we can actually run any of that.
And then finally here we're specifying what script we want to run.
In other words, what should we actually do in order to run these tests?
In this case run Python manage.py test.
Same command we ran in Django ourselves in order
to run the tests but by putting it inside of the Travis YAML file,
we're going to have Travis run those tests for us.
And so let's see what that actually looks like.
So what I have here is airline3, inside of which is this .travis.yml file,
which is exactly the contents of what was inside of the slide before,
specifying the language, specifying what version of Python,
saying install all of the dependencies first.
And then after that, go ahead and run Django's built in test.
This is what I want Travis to do any time I make a change.
So how do I actually configure Travis and set it up?
Well if I go to GitHub, you'll notice that I have on my profile
this repository called Airline which is going
to be the repository where I store my code for this airline application.
If I go to TravisCI.org now, which is where
Travis lives, and I go to my profile, I'll see my repository here.
Here is my repository, Airline.
And I'd like to enable Travis for this repository.
I want to start using Travis on this repository.
So after I've linked it up with my GitHub
account, which is fairly straightforward to do, I'll go ahead and enable that.
And what that's going to do is set it up so that any time I
make a commit or a push to GitHub, it's going to notify Travis And GitHub does
this through web hooks, where GitHub has this built in idea of hooks
on particular events on my repository where I can say,
anytime someone pushes to a repository, GitHub should know to do this.
And so in this case I'm saying, anytime I push to my Airline repository,
GitHub should notify Travis, the CI software that I'm using,
and let it know that there's been some new change that you
should run these new tests.
And so now on the command line I'll go ahead and add .travis.yml.
Because I want to now include this file inside of my repository.
I'll go ahead and commit and say I've added .travis.yml.
And I'll push those changes.
So now I've pushed those changes to GitHub.
And if I go here and go to this repository page, what we should see
is this build.
So a build is just Travis's way of saying it's detected the fact that
I've made a new commit, here's my commit, this add .travis-yml.
And now it's running a whole bunch of commands
in order to actually run these tests.
So it's first just doing some set up.
Here you can see that it's actually cloning the repository
by saying get clone, for instance.
And now it's checking what version of Python I have.
I have Python version 3.6.
The next thing it's going to do is right here,
it's running that command that we told it to run, pip install
-r requirements.txt, to say install all of those requirements.
And now after it's done installing those requirements
it's going to run Python manage.py test.
That line of code that I told Travis to run in order to say,
I want to run all these tests now and make sure that they actually pass.
I didn't have to do this myself.
Travis is running these tests automatically as soon
as I make a push to the repository.
And so what I see here is, OK, I failed a couple of tests.
So I failed the test flight page non-passengers test and the test flight
page passengers test, in both cases because zero didn't equal one.
And it told me I ran 10 tests and that two of them failed.
And therefore my build exited with status code one.
And so remember these exit codes that we talked about before,
where an exit code of zero generally means everything was fine.
Exit code of one, or some other non-zero number, means something went wrong.
If I run Python manage.py test and I pass
all of the tests, that command is going to return with exit code
zero meaning everything is fine.
And Travis will see that exit code and say, all right, exit code zero.
Everything must be fine.
We can therefore say we've passed this build.
This was all good.
Whereas on the other hand, in this case, if I failed some of my tests
for instance, and therefore Python manage.py test
exited with a status code, an exit code, that was not zero,
like one for instance, then Travis sees that.
It notices that something went wrong.
And therefore it reports that this build has failed.
So if I scroll up to the top, I see this red X. That means this build failed.
I didn't actually pass these tests.
And if I go back to GitHub and actually click on this commit history,
I can see that on this commit, this commit that I just added,
this add .travis.yml, on the right side of it now I see that red X that
actually indicates to me on GitHub that I did not pass all of the tests.
That Travis tried to run the tests and this build failed
and therefore it's reporting to me the fact that this build failed.
And we can use that information to our benefit
when it comes to things like pull requests
and merging between branches where you can start to impose constraints
in GitHub and say, in order to be able to merge from whatever branch
I'm working on to the original branch, this better not
be an X. I better have passed all of the tests
before I actually go about merging whatever change
I made back into the master branch.
And so what Travis is doing for me is automating
the process of running these tests such that now I notice,
OK, it looks like there was something wrong with my code.
So I can go back to my code now and say, OK,
if the problem was with my flight page I can check here.
What seems to have been the problem?
Well, it looks like I screwed up which one was passengers
and which one was non-passenger.
Just a mistake between which key was which.
This one should be passengers, this one should be non-passengers.
And now if I go ahead and add those files and say fix passenger list bug
and push those changes to GitHub.
And now I check GitHub's commit history.
So far I don't see anything there because the build's not done.
You'll notice that changes to this circle
to indicate the fact that the build is currently
running, that it doesn't know yet whether or not I've passed
or failed those individual tests.
But if I check back here I can see that now Travis
has detected the fact that I've made this new push, that I've
tried to fix the passenger list bug.
And now it's going to go ahead and, again, run all the requirements
and redo that exact same set of steps that it did before.
It's automating the process of building my code, running my tests,
and making sure that they pass.
In this case, it looks like it ran all 10 tests.
Nothing seems to have gone wrong.
It says OK.
Manage.py test exited with zero, so now we're done.
And if we go back and check this commit history,
now we see next to each one of these changes,
this add .travis.yml commit that I made, something went wrong.
I didn't pass the test here.
But now here when I tried to actually fix the passenger list bug,
then that did in fact pass.
So all is well in that case now.
And so what that's ultimately allowing us to do is automating this process.
Using continuous integration to say, any time I make a change, let's
just push it to some branch.
Maybe it's not going to be the master branch.
Maybe it's going to be some other branch.
But then let's run the test suit on it.
Make sure that the tests are all passing in order
to make sure that everything is in order before I try and merge things together.
With the goal being such that when I do merge things together,
I don't run into problems when I try and integrate everything
all together, that I've been checking all along with every change
that I make, am I going to break anything?
Am I going to break anything?
If I do I have the opportunity to fix it.
And if not, then I can feel confident at the end
that as long as my tests are thorough and comprehensive,
that my change is not going to cause any problems with the rest of my web
application.
Questions about any of that so far?
About what Travis or other continuous integration tools are used for?
Travis certainly isn't the only one.
Just happens to be one of many.
But they're all with the ultimate goal of doing things
like this, of automating the process of integrating code together.
And they can be used for other things as well.
Travis can be used in order to make sure that we are sending notifications,
for instance, such that Travis can notify a slack channel, for instance,
to say that this succeeded or this failed.
It can also be used for automatically deploying an app.
So if you're using Amazon AWS in order to deploy your application to a server
somewhere, Travis can take care of that for you
where it can say if all of the tests passed, then let's go ahead
and deploy this application to the internet and automatically deliver it.
And if the tests didn't pass, then let's not do that.
Let's not actually push those changes because we
want to make sure the tests are passing before we go ahead
and make those changes.
Questions about any of that?
OK so what other issues might we run into when
we're thinking about developing software, about building applications,
deploying applications to the internet?
Well, one thing that you may have already come across
is just making sure the application even runs in different environments.
If I took one web application that I've just written and put it
onto a different computer and then try to run that Django application,
there is no guarantee that that application
is going to behave in the same way.
Maybe the computer that I put it on doesn't even
have Django installed, for instance.
And so I'm going to get an error if I try
and run the server because Django doesn't exist on the computer
that I'm using.
Or maybe it's using an older version of Django whereby as a result
some of the newer features of Django that I'm using
aren't going to work on the older computer.
And so in web application development you
might run into all sorts of these problems
where the computer you're running things on,
or the computer you're developing locally on,
might be different from the computer where you're actually
running the application in production, where things are actually
running that users are really using.
And as a result you might run into all these sorts of compatibility errors
between your machine and some other machine
because there is no guarantee that different machines are
behaving in the same way.
When really we would like to make sure that when I'm running my application
it's going to be working in the same environment the whole time.
That if I have these dependencies and libraries installed
on my application on my computer, then someone else running my application
on a different computer will have all of those same things installed,
will have the same set up ready to go without any of those issues.
So how might we go about achieving that?
What are some strategies we could use to make
sure that the way I'm developing my application
is going to be in the same sort of environment to someone else?
Multiple possible things you could throw out here as ideas.
Yeah?
AUDIENCE: Virtual machine.
BRIAN YU: A virtual machine, yeah.
And for a long time, a virtual machine was really
the best way to do something like this.
That you would, in a virtual machine you would set up a virtual operating
system, the guest operating system that was operating
on top of your existing operating system where you could virtually
run your own computer that had whatever configuration settings you wanted it
to.
And you could therefore say, this virtual machine
is going to have these dependencies installed on it.
It's going to be running this operating system.
And so when I run my application on that virtual machine
then I can be sure that if I'm running it on this particular virtual machine
then everything is going to behave exactly as expected
because the machines are identical.
We're running the same virtual machine on my own computer
and on wherever I'm trying to run the application.
What might be some drawbacks of the virtual machine
that we can think of, for running a web application like a Django web
application on the internet?
Yeah?
AUDIENCE: [INAUDIBLE].
BRIAN YU: It's slower, yeah.
It tends to be pretty slow because I need
to start up this entire virtual machine with an entire operating
system of its own, with its entire any libraries that
are necessary for the operating system.
And generally that can be overkill if all I want to do
is run a web application, for instance run a simple Django application that
just needs to have certain dependencies installed.
And so one solution that people have come up
with in order to deal with that problem is containerization.
The idea of creating containers that are not quite virtual machines in the sense
that they're not fully blown operating systems on top of which
are all these additional layers but are just
isolated containers that have just the things
we want to have installed on them.
Such that if we define what should go into a particular container
inside of what we'll call an image, we can make sure that the image
that I'm using on my computer is the same
as the image that's being used on the server
that my application is eventually running on.
And therefore I can be confident that whatever environment
I'm running my application on my local computer
will be the same for anyone else who's also running the application as well.
And so one of the most popular tools that I'll introduce now
for containerization is called Docker.
And the way that Docker works is-- we can use this as an example
by comparing Docker to a virtual machine.
So on a virtual machine we have the original computer on which you're
running your own operating system.
And on top of that, for each application you would run,
you'd have a virtual machine which has a guest operating system on it
on top of which are all the libraries that you might need.
And then the application is running on top of that.
Whereas the way Doctor is going to work is
that Docker is just going to build directly on top of the operating
system on which you're running.
It's going to have direct access to the operating system
that your computer is running on.
And it's just going to add an additional layer that
helps to keep these containers isolated from each other
so that each container can still have its own libraries and dependencies.
I can say, inside of this container I want to install version two of Django,
for instance, and then run my application on top of that container.
So it's a slightly different model but it's still
trying to get at the idea of, separate out
our applications into these containers where each container
has just the things that it needs to have
installed in order to run that application in such a way
that it's consistent.
Such that so long as I know what the instructions
are for creating one of these containers,
then when I need to run my application somewhere else
I can just create one of these containers
and then run the application.
And this will also be useful when it comes towards applications
that have multiple different parts.
So for instance in project one where you were
using a PostgreSQL database and a Flask web application,
there were sort of two different parts to that web application.
We had the Flask application itself.
And then we also had your database running in some separate server
elsewhere.
And what Docker is going to let us do, and what we'll see in a moment,
is let us take all of those different services
and compose them together, so to speak, in such a way that so long
as you have the instructions for how do you start up the web application
server?
How do you start up the database server?
It can do all of those things on its own such
that you don't need to worry about whether or not
you have all the services connected in the right way.
So we'll take a look at some concrete examples of that
by taking our application for airlines and Dockerizing it
by using Docker images and containers in a moment
to explore how we can actually take advantage of Docker to make it easier,
ultimately, to deploy our application.
The goal of all of this is going to be to make it easier to test and deploy
our application in such a way that everything is all in one place,
that we don't need to worry too much about compatibility issues that
might arise.
So let's take a look at airline4 now.
And we'll open that up.
And so the first thing that you'll notice that's different about airline4
is that I have this file called Docker file inside of my repository.
And so Docker file is going to be the file that's
going to define a Docker image, where a Docker image you can think of it
as just the instructions for how I'm going to create one of these containers
that my application is going to live in.
So here are the instructions inside this Docker file
for how to create a container that I want to run my web application in.
So what I'm going to say is, from Python:3 it's just saying,
from is going to tell me sort of like inheriting from some existing Docker
image.
There's an existing Docker image called Python 3 that's
just a default image for creating a container that
runs Python 3 applications.
And I'm going to use that.
I'm going to set my working directory, the directory that I'm in,
to a user source app.
So this app directory is just going to be the directory where
I'm running my application.
I could have used some other directory but this just
happens to be a convention to put everything inside of this app
directory.
And now what do I want to do to make sure
that whenever I'm running this application
I'm always going to be running it in the same environment
so I have the necessary dependencies installed?
Well the first thing I wanted to do was make sure
that I've got Python 3 installed because I want to be
using Python 3 for this application.
But I also want to make sure that my requirements are installed.
That if I need Django installed in order to run this Django application, then
I better first add requirements.txt to my app directory.
And then I'm going to run pip install -r requirements.txt, to say go ahead
and run the installation of those requirements.
Such that now if I were to ever create a Docker container based
on this Docker file what that's going to do is install all the requirements.
Where if I look at my requirements file I have a Django version two and also
a Python library that's going to be useful for dealing with PostgreSQL
if I have a Postgres database, for instance.
And finally, the last step is going to be add all the contents of my current
directory, dot meaning the current directory,
to that app directory as well such that all of my Django files--
my application files, my settings files, URLs and views and templates and all
that--
gets added to my application directory.
And so that is going to be how I define--
this is going to help me define my Docker image such
that when I create a container it's going to run all of these instructions,
first installing those requirements and then adding
the entire contents of my application to this app directory
inside of my container.
So how do I then say what my application needs to do?
So let me first show you settings.py.
So settings.py is one of those files we took a look at in Django
before that lets us define various settings for our application.
Last week inside of settings.py we specified
that our database would be SQLite, which is a simple database and very
easy to use when testing.
But in reality if you were to develop a web application that
was going to be used by a large number of users,
SQLite's probably not the best one to use.
You'll probably want to use a database that will scale a little bit better.
And for instance, Postgres is a good one for that,
which is what we used in project one.
And so what we might do is change our database, for instance,
to instead of using SQLite as our database
that we defined in settings.py, we might instead specify inside of our settings
file that we want to use PostgreSQL instead.
So instead of engine, which with SQLite before,
now I'm going to specify Postgres.
And I can use name and user, Postgres is just
the name and user name of the database.
It might be something else.
But in this case, we use Postgres.
And specifying the port on which that database is run.
And so depending on where your database is,
these might be a little bit different.
But you'll see how this information is going
to be useful now because what I want to do, now,
is tell Docker that when I start up this application
there are a couple of different things that I want to do.
On one hand I want to start up the database.
I want to make sure that my Postgres databases is
up and running, because my Django application is
going to need to talk to it.
Secondly, I'm also going to want to actually start up the web application
itself because I want to make sure Django
is up and running, that that server is up
and running for when I want to interact with it.
And finally, there's actually a third thing
that I might want to do which is I might want to,
anytime I run my application, to automatically run any of the migrations
that I need to apply.
Such that if I bring my web application to some new place
where it's got some new database, I want to make sure that any
of those migrations that I had-- migrations
being ways of applying changes to the existing tables that
existed inside my web application.
I want those migrations to be applied to my new database.
And so I would like it for whenever my application starts up,
go ahead and migrate everything over, any
changes I've made to those tables, such that I
make sure that my web application is running
on the latest version of my database.
So those are all of the different services
that I want running when I run my Python application.
And the way to make all of that come together
is by using what's called Docker Compose.
And so DockerCompose.yml will be the last new file that we'll look at today.
Which is going to be a YAML file in the same file format as before,
which is going to define all of the different services that make up my web
application.
And so the first service that I'm going to have is a database.
And I need to specify what Docker image should
I use in order to construct this database.
We'll go ahead and use Postgres, which is
just one of the default ones for just creating a Postgres database.
In addition to the database, I also want to set up a service
for migrating everything over such that when I start up
an application, whether it's on my local computer or on a server somewhere,
I automatically migrate any changes.
And so build dot means I'm going to build based on the Docker
file in my current directory, because I have this Docker file here
that's going to tell me how to install all the requirements, for instance.
The command is going to be what command to actually run
to make the service happen.
And so I'm going to run Python manage.py migrate in order
to run all of those migrations because that's what I want to happen every time
I start up the application.
Volumes helps to link between different files.
So I'm saying, link between the current directory
that I'm in with this app directory, which is where in the Docker file
I said my container was running.
And then depends_on is saying, what should be done first
before I actually work on this service?
And in particular, I need this DB service running, this database service
running, which is my Postgres database to be working
before I do my migration because I want to make sure the database is there
before I try and migrate anything there.
So now I've set up the database.
Now I've set up the migration such that when I start up my application using
Docker it's going to create the database as its own container,
then migrate any changes that need to be made.
And now let's add another service just called
Web, which is going to be the one that's actually running the web application.
So again that's going to be based on the Docker file in my current directory.
What command am I going to run?
I'm just going to run that same Python manage.py run server.
And specifying what IP address and port I want it to run on,
just setting that explicitly though you might not necessarily need to do that.
And then the only different things here are what's happening here in ports.
What this is saying is, my container has its own set
of ports where things are running on.
And what this is saying is, take port 8,000
that is running inside of my container and map
that to port 8,000 that's running on my own computers.
So that if I, on my computer, go to this URL
I can actually access the web application that's
running inside of that container.
And then finally, I'm saying this depends on two things.
It depends on the database being there.
I want to make sure the database is there first.
And I also want to make sure that the migration was successful.
So make sure the database is there, make sure
that I've migrated everything over.
And once that's done, then I can start up this web application
by running the server.
And so this is a lot of new syntax.
The general idea-- don't worry about memorizing this--
is just getting a sense for what Docker is designed to let us do.
Where the idea is that it's a way to allow us to easily create containers
that have all of the requirements and dependencies
that we need by just automatically installing whatever requirements we
have, making sure we have the right version
of Python installed, and then, in Docker Compose,
defining the different parts that make up my web application-- defining
the database that I have, any migrations that I might need to perform,
and the web application that I actually want to start up.
And so now in order to start up my application,
I can run something like Docker Compose up,
meaning start up my web application using Docker Compose.
It's going to look for that DockerCompose.yml file.
And what you'll see is the first thing that it's
going to do is start up the database.
Then it's going to start up the migration.
Then it's going to actually start up the web server.
And now I can go to 0.0.0.800, and now I see
that I have this interface where right now I don't have any flights
but now I'm running my web application using Docker.
And so all I've done so far is just start up
the web application, which doesn't seem like I've gained a whole lot.
But what I have gained is, I've gained at these two files, the Docker
file and the Docker Compose file, that define exactly how to start up this web
application.
And this is using a Postgres database which
I didn't need to specifically download Postgres and install it and figure out
how to use it.
Docker is taking care of that for me by just creating a container that's
running a Postgres database.
And what this means now is that I can give this repository to anyone.
And so long as they have Docker installed
they can run this web application and expect
it to behave in exactly the same way.
And so this is very useful where, in web applications,
it can often be a challenge to make sure that the way an application is behaving
on your computer will be the same as the way
the application is behaving on different computer.
By taking advantage of containerization, we
can A, simplify the process of combining these services together, but B,
simplify the process of making sure that the application is
going to behave the same way across different places as well.
Questions about what Docker is, what it's used for, the general ideas here?
Yeah.
AUDIENCE: [INAUDIBLE]?
BRIAN YU: Good question.
So who is actually going to be using Docker?
So, if your project uses Docker any developer working on the same project
is likely also going to be using Docker.
Whereby instead of running Python manage.py run
server, which is going to run a server based on whatever libraries
you have installed on your computer, you will instead
use something like Docker Compose up to start up the Docker container
and then run things inside the Docker container.
And that way if there's something missing from your Docker configuration,
if you're missing a dependency for instance, it won't run in Docker
and you'll know that you need to add added dependency, for example,
to requirements.txt.
And once you do, then it will behave the same way on someone else's machine.
Then on the deployment side, all sorts of application Deployment Services
now are able to use a Docker image and build the application based
on that Docker file.
Such that some common ones include Heroku, for instance,
is a common service for deploying applications to the internet.
Amazon AWS is also a very popular service for taking web applications
and deploying them.
And normally, you might have to worry.
Do the servers that Amazon's running things on
have the same version of all the software that I'm running things on?
There are ways now with Amazon AWS to say,
let me just give Amazon AWS the Docker configuration for my application
and now Amazon can use Docker to ensure that when
they start up the application on their servers,
that it's going to behave in exactly the same way
because we defined exactly what should be inside the container.
And therefore we can be confident that that behavior will be emulated when
the application is run on the server.
Yeah.
AUDIENCE: [INAUDIBLE]?
BRIAN YU: Great question.
Are all of the services running in the same Docker container?
They're not.
So in fact, if I go ahead and open up a new tab and I run Docker ps,
that's going to list for me all of the Docker containers
that I currently have.
And I'll just shrink things down so it's all on one line.
And what you'll notice is that I have two containers that are up right now.
Here's one container that's based on the airline4 web
image, which is the image based on the Docker file I showed you a moment ago.
And then I also have this Postgres image,
which is going to be the database that I have running.
So I have two different containers, one that's
running the actual Django web application
and one that's running the Postgres server.
But they're able to communicate with each other.
And if I wanted to actually get inside of one of the Docker containers
in order to run commands in it, for instance,
that would look something like this.
I could say, Docker exec--
meaning run a command--
-it to say I want to be interactively working in a terminal.
Paste in the container ID of the container that I want to get into.
And then I want to run bash, just run a shell.
And now you'll see that I'm inside of the container now.
I'm inside of the container, inside of this user source app directory.
And now I can do all the same things that I could do before,
where if I want to go into Python manage.py shell,
I'm now inside of the shell of the Django application
that's running inside of this container.
And so if you ever need to get into a container
in order to run commands in order to make modifications so on and so forth,
you can do something like that in order to get access to it as well.
But the long story short answer to your question is that each of these services
are running separately in its own container.
AUDIENCE: [INAUDIBLE]?
BRIAN YU: Good question.
If I was doing development, would I want to go into the container to do it?
I don't need to get into the container in order to use the web application,
just to go to the 0.00:8000, because I've linked these ports together.
But I might want to go into the container
if I needed to modify the database, for instance.
If I wanted to manually add a new airport or flight, for instance,
I couldn't just say, inside of my airline4 directory now,
I couldn't just say Python manage.py shell
and go there because this is going to be the shell for the Django application
that's running on my computer which is different from the database,
for instance, that I have running inside the Docker container.
So if I wanted to go into the Docker container
in order to modify things inside the database,
then I would need to first enter that container.
And then I could modify things that are specifically
happening within that container.
AUDIENCE: So in this case, you have a full version of the [INAUDIBLE]??
BRIAN YU: Good question, yes.
So the question is, I have a full version of Postgres
contained inside the container?
And yeah, inside of this Postgres container here, this one,
I have an entire Postgres server running and storing the data
and Docker knows where things are located and just abstracts all that
for us.
We don't need to worry about where exactly this is happening.
But it's effectively just the same as, whereas in Heroku
when we set up a database there we were able to access it
because it was its own database server that was running.
This is the same thing.
It's just a database that just happens to be running in a container
that Docker is running for me on my own computer.
And because it's following these strict instructions
for how to create the container based on this image,
then when someone else tries to create the same container
they'll be in the same environment and everything will work the same way.
OK, so all of these tools of testing, and Travis,
and other CI software and Docker and such,
are designed to be part of this CI/CD pipeline.
Of making it easy to take changes you make to your code,
integrate them together frequently in order to run automated tests on them
to make sure that they're working, make sure
that when you are running those automated tests that they're
running on the same environment.
Travis also has ways of integrating with Docker, for instance,
so that when you're running tests on Travis
you're making sure that the application running on Travis
is running in the same environment as the environment
that you're developing on, or the environment that your application is
eventually running on.
And using Docker, that makes this process a whole lot
easier by making sure we have consistent environments
for when we're running tests or when we deploy an application to the internet.
And these are all just good software design practices
that come in handy for making sure that as we
begin to work on larger, more complicated web applications, that we
have the tools in place to make sure that we're avoiding bugs,
that we're making sure that our application works
at every stage in the process, and that the deployment process is
clean and ultimately efficient.
So we'll wrap up there for today.
Thank you so much.
And we'll have some special guests for you next week as well.
Thank you.