Placeholder Image

Subtitles section Play video

  • [MUSIC PLAYING]

  • JORDAN HAYASHI: Hello, and welcome for lecture 12, deploying and testing.

  • So last week, we talked about performance.

  • We talked about the trade-offs that you get

  • when you do some performance optimization, that

  • just being additional complexity.

  • We talked about a couple of different ways to test against performance, one

  • being the React Native perf monitors.

  • So it's built into your phone, and it allows

  • you to see how many frames per second you run.

  • We talked about the Chrome Performance Profiler,

  • which actually runs your JavaScript code within Chrome

  • and shows you a flame chart of all of the components that are rendering.

  • We talked about a few different common inefficiencies that we see in React.

  • One would be re-rendering too often, another

  • being unnecessarily changing props, which may actually

  • contribute to re-rendering too often.

  • And then lastly, just unnecessary logic.

  • And then we ended the lecture with a demo

  • of this animated API, which allows us to run

  • some calculations on the native side of things rather than within JavaScript.

  • So this week, we'll give a high-level talk about deploying apps.

  • So the way that you deploy in Expo is by building the app locally, and then

  • uploading it to the store.

  • Before you do that, you have to make sure to set the correct metadata

  • in your app.json file.

  • And you can find out exactly what those are at this Expo documentation link

  • here.

  • Then you go ahead and build the app using

  • this command-line utility called exp.

  • It's basically just an alternative to the XDE

  • because the XDE does not allow you to build the app within that GUI.

  • The way to do this is to install the script.

  • So you do npm install global exp, just like how

  • you install a bunch of other libraries.

  • You then build it by doing either build colon iOS or exp build android.

  • What that does is it actually sends your JavaScript--

  • your whole bundle, actually-- over to Expo's servers,

  • where it does the entire build.

  • And then Expo uploads that build to S3.

  • The way that you get that build onto your computer is to run this [? ex ?]

  • [? build ?] [? status, ?] which if you run it while it's building,

  • it will let you know it's still building.

  • But once it's done, it will spit out a URL for you,

  • which is the link to your app in Amazon's S3 storage system.

  • And then you can just cut and paste that into a browser

  • and go ahead and download that file.

  • Then you can upload it to the appropriate store.

  • So the way that you do that depends on whether you're doing iOS or Android.

  • And here's a couple of documentation links

  • that talk you through that process.

  • One great thing that you can do with Expo

  • is to deploy new JavaScript by republishing.

  • And that's called over-the-air update.

  • So you actually upload a new bundle because the way that your app works,

  • it's really just a shell that's running JavaScript code.

  • And so you can go ahead and download some new JavaScript from the web

  • and run that as your application.

  • And so by republishing from the XDE or using that exp script,

  • you can deploy a new JavaScript bundle and push it

  • over the air to your clients without having them redownload

  • the app from the app store itself.

  • Though if you want to change any application metadata,

  • like the name of it, then you're going to have to resubmit it to the store.

  • So doing this over-the-air update thing is very, very powerful,

  • but also slightly dangerous because you can actually upload JavaScript

  • that crashes your app and doesn't work.

  • And it won't actually be caught by their respective app store people

  • because they never see it.

  • It just goes straight to the cloud.

  • And then your users go ahead and download that.

  • And so how do we ensure that everything works as expected before deploying?

  • So you do that by testing.

  • And so when we use the word "testing," we generally

  • are referring to automated testing as opposed

  • to manual testing or doing quality assurance,

  • whereby you just open up your app manually and play

  • with all of the features that you have to make sure

  • that they work appropriately.

  • And so why might we want to do this?

  • Well, as your application grows in size, manual testing

  • gets more and more difficult. And more complexity

  • means there are more points of failure.

  • And so if you imagine if every single week,

  • you add 10 more features, that's growing linearly every week.

  • And so even though you keep adding the same number of features every week,

  • the amount that you have to test every week actually

  • goes up by the integral of that.

  • So you have a linear growth in the number of features

  • that you have to test every single week.

  • So by adding a test suite, you can ensure that you catch all of your bugs

  • before they get shipped.

  • Because the last thing that you want to do

  • is actually upload some broken code, because then all of your users

  • will get a broken app.

  • But how do we know which parts of our app to test?

  • There's this theoretical structure called the test pyramid.

  • And it determines a methodology for determining test scale or granularity.

  • So at the very bottom of the pyramid is what's called a unit test.

  • And this tests an individual unit of code.

  • And generally, that's either a function or a class or a method.

  • Then you have integration or service tests,

  • which allow you to test the integration of multiple of these pieces of code

  • and how they work together.

  • And this is completely independent of the UI.

  • Which brings us to the last one, the UI tests, or end to end.

  • And this allows us to test a feature thoroughly,

  • including the UI and network calls and everything.

  • So it's basically from one end of your code all the way to the other,

  • basically what a user would be doing themselves.

  • So how are we going to write some unit tests?

  • And so as we talked about in the last slide,

  • this is testing an individual unit of code,

  • like a function or a class or a method.

  • One great thing about unit tests is they're very, very granular.

  • So you know exactly where your code is breaking.

  • And that makes it very easy to tell what you need to go fix.

  • And so the very most basic unit test is just

  • a function that lets you know when any behavior is unexpected.

  • And so let's go ahead and write our first unit test.

  • So I'm going to make a new directory called testing.

  • And inside that, we're going to go ahead and write our first test.

  • And so let's write a very simple function, maybe something like sum.

  • And what sum does is it's just a function that

  • takes a couple of numbers.

  • Let's do function sum.

  • That takes an x and a y.

  • And let's just return the sum of those two numbers.

  • So return x plus y.

  • So pretty simple.

  • I'm pretty confident that this code does what I think it does.

  • Let's go ahead and write some tests anyway.

  • And so how might we go about doing that?

  • Well, we could write a function that does that.

  • Or we could just do console.assert, which is basically saying,

  • I'm going to assert that this thing is true, and if it's not true,

  • then throw an error.

  • And so let's just assert that sum of 1 and 1 gives us 2.

  • And if it doesn't, then we can throw an error that says error summing 1 and 1.

  • So we can save this and run it using Node.

  • And we get no output, which is a good thing because if there's no error,

  • it means our assert passed.

  • And so let's see what this looks like if we had written

  • assert that doesn't pass.

  • And so say we asserted that 1 and 1 was actually 3.

  • And let me go ahead and quiet this ESLint.

  • So if my now assert that sum of 1 and 1 is 3, then when we run this,

  • we get an assertion error.

  • It says assertion error.

  • Error summing 1 and 1.

  • And so we just effectively wrote our very first test.

  • So let's first fix this and make sure it passes and maybe write a couple

  • other tests.

  • We can do console.assert that summing 0 and 0 gives us 0.

  • And maybe a last one that summing 20 and 30 gives us 50.

  • 20 and 30.

  • And we can just sanity check to make sure this actually works.

  • And it does.

  • Great.

  • So we just wrote our first simple test.

  • And so one thing that you may notice is that we have our tests directly

  • in the same file as our actual function.

  • And so if we were to write something like add with an x and y--

  • or not add, but maybe multiply, now if we want to add a test to this,

  • we're going to have to add some console.asserts.

  • And suddenly, this single file starts to get a little bit noisy.

  • And so just like we've done in other examples,

  • we might want to start breaking out things into separate files.

  • And so maybe the test should not be in the same exact file as the functions.

  • Maybe we should add it to its own test file.

  • And so let's go ahead and do that here.

  • So let's save that, and then create a new file called sum.test.js.

  • And let's just move all of our asserts over to this file.

  • And you may notice that if I try to run that sum.test.js file,

  • it's going to error because sum is not actually defined.

  • And so we're going to have to bring that function called sum

  • from sum.js into its test file.

  • And since we're working in Node, we'll go ahead

  • and use that require statement.

  • So const sum equals require ./sum.js.

  • And then just make sure that in test.js or in sum.js,

  • we need to just make sure that we set our module's exports to be sum.

  • And so this is basically the same way of doing export default in ES6

  • is just the way of writing it in Node, since Node doesn't yet

  • support import and export syntax.

  • So let's just do module.exports equals sum.

  • And then now we can try to do node sum.test.js, and no errors.

  • Great.

  • So what happens if we change sum.js?

  • Maybe we actually add a bug where rather than doing x plus y,

  • we do x plus y plus y.

  • Now if we run our tests, we see that we get an error summing 1 and 1.

  • But it doesn't run the rest of our tests.

  • It just stops at the first error.

  • And so that's a little bit of a downside of the way

  • that we've currently been implementing our tests.

  • And so there are these things called testing frameworks

  • that give you additional benefits.

  • One of them is that they run all of the tests

  • instead of just failing on the first error, like we just saw.

  • Another great thing is it gives you pretty output because currently, we

  • just get this assertion error, which is informative,

  • but it doesn't really look all that great.

  • It also has this cool benefit where it can automatically watch

  • for any changes in our application.

  • So as we change our logic, the test can run automatically

  • without us having to remember to run them before even checking them

  • in or sending them in a new deploy.

  • And lastly, they give you these things called mock functions, which we'll

  • take a look at it in a little bit.

  • So the testing framework that we're going to be looking at this lecture

  • is called Jest.

  • And so if you look up Jest, what you see is

  • this thing called Delightful JavaScript Testing, which is a bold claim.

  • But we'll see that it is pretty great to work with.

  • So this is a testing framework written by our friends at Facebook,

  • so the same people who wrote React Native.

  • And the way to install it is doing npm install --save-dev jest.

  • And so just to do that here, we can do npm install dev jest.

  • So there's a shorthand to just do npm i -d rather than npm install --save-dev.

  • And so this goes ahead and installs Jest.

  • And as it's installing, the way to run it is doing npx jest.

  • Make sure if you want to use the npx command, it's built into NPM 5

  • and above.

  • And I believe can install it otherwise.

  • But in NPM version 5 and above, it's automatically bundled.

  • But the easier way to run it is just by adding a script to your package.json.

  • And so just like in earlier examples, we added something

  • to package.json, which might be, like, linting.

  • We can also add something like test, where we say if we run npm run test,

  • it's just going to run Jest.

  • Jest.

  • And so now we can run npx jest, and we see that it does run.

  • It actually ran our sum.test.js file because it's

  • smart enough to know that anything something.test.js, it should just run.

  • Or we could run npm run test, and it will do the exact same thing.

  • There's actually shorthand for this.

  • We can do npm test.

  • Or even shorter hand, we can do npm t.

  • So all of those four commands all do the same exact thing.

  • So the way that this works is it will automatically find and run any files

  • that end in .test.js or any other regex expression that you specify.

  • But for now, we're just going to be using that .test.js.

  • And so now let's rewrite our sum.test.js.

  • Rather than using console.assert, let's go ahead and use Jest.

  • So the way that we do this is we just replace that console.assert with jest.

  • And so the syntax for that is to define a test.

  • So you just do test, and then a string of what you're actually testing here.

  • And so let's do--

  • let's test that sums 1 and 1.

  • And now we have a test for summing 1 and 1.

  • We can do the same thing for sums 0 and 0, and lastly, 20 and 30.

  • And if we now run this, we see that it will find the tests.

  • And it just skips all of them because we didn't actually

  • specify what they were supposed to do.

  • But it went ahead and found all three of the tests.

  • And so now let's go ahead and define what those tests should actually do.

  • So how are we going to define this test summing 1 and 1?

  • So first, let me go ahead and let ESLint know that there

  • are a few global functions called test.

  • So now we can say, how are we going to test that we correctly sum 1 and 1?

  • And so the way to do that is to pass a callback.

  • And we can say we're going to expect that if we sum 1 and 1, it should be 2.

  • And so the syntax for doing that in Jest is

  • you can do expect something-- so the expression here is 1 and 1.toBe,

  • and then whatever we expect it to be.

  • So in this case, it will just be 2.

  • So we can save that, and then let ESLint know that there are

  • a couple other globals called expect.

  • And then now if we run this, we can see that it finds it, and it runs here.

  • We expected it to be 2.

  • We got 3.

  • Why is that?

  • Because a while back, in order to show that we can fail a test,

  • we actually changed our implementation of sum.

  • And so we should probably fix that to make it correct.

  • So great.

  • Our test caught an illogical bug.

  • So now let's go ahead and run npm test again,

  • and we'll see that now it passes.

  • And it gives us a nice little green check mark next to our sums

  • 1 and 1 test.

  • So let's go ahead and add the rest of our tests.

  • So here, we expect this to have a value, as well.

  • So we can say expect sum 0 and 0 to be 0.

  • And lastly, summing 20 and 30, we expect that if we sum 20 and 30,

  • we expect that to be 50.

  • And so now our tests are actually a little bit more readable.

  • You can basically just read the code, and you have the tests.

  • So test that it sums 1 and 1.

  • How do we do that?

  • Well, we expect the sum of 1 and 1 to be 2.

  • So it reads almost like English.

  • So let's go ahead and run all of these.

  • And we see that all three pass here.

  • And so I talked about how Jest can automatically watch all these files.

  • But I haven't been doing it.

  • I've been quitting my file, then rerunning npm test.

  • And so let's also add a script that allows us to watch the files.

  • So the way to do that is we can just do npm test,

  • and then pass it a command-line flag called watch.

  • And we'll go ahead and run jest watch for us.

  • And now it'll run it.

  • Let me first quit and create a separate pane.

  • So if I do npm test and tell it to watch, then it will start watching.

  • And then I can flip over into a new pane, open up sum.test.js.

  • And if I add a new test--

  • let's do 20 and 22.

  • And we expect that to be 42.

  • I'll save, and I'll hop over to the new frame.

  • And we see that it already ran that test for us automatically.

  • And so it's watching all of our files, and anytime one of them changes,

  • it will run that associated test file.

  • And maybe we don't want to have to run npm test --watch

  • every single time we want to do that.

  • We can just add another script to our package.json.

  • So if I add a new script here that is test watch--

  • I can name this whatever I want.

  • I'm just going to call it test watch.

  • We can just run jest --watch.

  • And it'll do that for me.

  • And so you see here that we have jest with a single flag, --watch.

  • The reason that we had to do two flags above like this was this

  • would let us let NPM know that it should pass the next flag to whatever script

  • it ends up running.

  • But now we can just run npm run test watch.

  • And it does that for us.

  • Great.

  • So let's forge ahead and actually test some things

  • that we've been using thus far.

  • So let's go ahead and test our Redux actions.

  • And so we can replace any of our tests that we

  • have prior, which we don't have any yet, with expect to be and to equal.

  • And so let's go ahead and write some tests for our Redux actions.

  • So rather than writing in a dummy sum file,

  • let's actually open up our Redux, our actual actions,

  • and start writing tests for these actions.

  • And so if you remember from prior weeks, we

  • have a couple action creators that are functions that take an update

  • and return a type and a payload in the case of update user.

  • In the case of add contact, it takes a new contact

  • and returns an object or an action with a type and a payload.

  • And then lastly, we have an async action creator

  • that does a bunch of additional things.

  • So let's go ahead and write a few tests for our action creators.

  • So let's first open actions.test.js file.

  • And now let's write some tests for our Redux actions.

  • And so what's the first one that we want to test?

  • So let's just remember which exist.

  • So the first one we're going to test is the simple update user.

  • And so let's go ahead and first import.

  • Let's just import all of our actions.

  • So import everything as an object called actions from this file called actions.

  • And then let's do some testing.

  • So let's do test that update user returns the correct an action.

  • And how are we going to do that?

  • Well, it's just a function.

  • Let's invoke update user.

  • So let's expect that if we invoke update user, and what are we going to pass?

  • Let's just pass the name should be updated to test name.

  • And what do we expect that to be?

  • Well, we expect it to be an object that has a type of actions.update user

  • and a payload of name test name.

  • And we're missing parentheses there.

  • Great.

  • So let's run this test and see what happens.

  • So if we run it, it will let us know something interesting.

  • Well, one, the updateUser's not defined because it

  • should be actions.update user.

  • So since we're importing all of the actions

  • as an object called actions, if we want to use updateUser,

  • we need to do actions.updateUser.

  • And while we're at it, let's let ESLint know that there are

  • a few globals called test and expect.

  • And now let's rerun those tests.

  • And now it will tell us something interesting.

  • It says we expected the value to be this object.

  • And instead, we received this object.

  • And you might notice that these two objects are exactly the same, character

  • for character.

  • And so why might it have errored?

  • So if you remember back to an early lecture,

  • we talked about how to compare objects.

  • And the way if you compare objects with triple equals, what does it do?

  • It doesn't compare the key value, all of those values within the object.

  • It actually just checks if the two objects

  • are being referenced in the same location

  • because anything that is a non-primitive is stored via reference.

  • And so since we created a new object in our .toBe in our test file, we see, oh,

  • object is equality.

  • They're not referencing the exact same object.

  • And it actually lets us know, hey, the compared

  • values have no visual difference, meaning

  • they're basically the same object here.

  • Looks like you probably wanted to test for the object array

  • equality, which is--

  • we should probably use toEqual, which actually does that check

  • and will check the key-value pairs rather

  • than just checking the reference.

  • And so let's go ahead and update our code so that it's actually correctly

  • checking what we want it to check.

  • So let's now do actions.test.js.

  • And rather than using .toBe here, let's use toEqual.

  • And now let's run npm test.

  • And we should see that now it actually does indeed pass.

  • Great.

  • So let's add a few more tests.

  • Maybe let's pass a different name here.

  • Maybe we'll update a phone number.

  • And let's make it some bogus phone number.

  • And then we want to change the payload here to be that phone.

  • And what do you notice that I'm doing right here?

  • It's interesting because I'm almost reimplementing this action, right?

  • My test is basically the exact same logic as the actual actions file.

  • Because our updateUser takes an update and returns

  • an object with that type and payload.

  • And down here, we're doing almost the exact same thing.

  • We're doing an action with this and checking against an object

  • with a type and a payload.

  • And so we're almost word for word just reimplementing that exact function,

  • which isn't really great in terms of testing

  • because if we're testing a function by running a function

  • that we implemented in the exact same way,

  • if there's a bug in the first function, of course there's

  • going to be a bug in the test function.

  • And so we won't actually catch any bugs that we might have intended to catch.

  • And so what might be a better way to test this?

  • Well, it turns out there's this thing called snapshots,

  • which compares an output of a function in the past to what we get now,

  • which is good because now we get notified if the output changes,

  • which is really what we want when we test these action creators.

  • Because since they're really simple and just returning a new object,

  • we just want to make sure that it's returning what we expect it to return.

  • And so then if we end up changing something later,

  • we get notified that something changes, but we don't actually

  • have to rewrite the test if that change was actually intended.

  • And so let's actually refactor our test to rather than

  • mirroring the logic in our actions, to, rather, use a snapshot.

  • So currently, we're doing expect this to equal,

  • and then hard coding exactly what we know is going to come out.

  • But maybe instead, we should do toMatch the snapshot.

  • And we can go ahead and delete that.

  • And so if we now save that and run npm test,

  • we see that the snapshot was saved.

  • And now let's change it to see if the snapshot--

  • if we're notified, as expected.

  • So let's change our actions file.

  • And maybe we'll send a type that's updateUser, a payload that's update,

  • and also--

  • we wouldn't want to do this in real life.

  • But maybe we'll just add a debug flag that says false.

  • So something completely useless, but it is in fact changing our implementation.

  • And so now if we run npm test, we'll see, uh-oh.

  • One of our snapshot tests failed.

  • Let's see what happened.

  • In our actions.test.js, we see that update in our test

  • called updateUser returns an action.

  • We see that the received value does not match the stored snapshot.

  • And it'll actually tell us exactly what changed.

  • It says that the snapshot received something new.

  • That's what this plus received is.

  • And what it received that was new was this debug false,

  • which wasn't in the original snapshot, which is great because maybe we

  • didn't want to change that.

  • It's just letting us know that it changed.

  • And so how do we let Jest know, hey, we meant for that to happen?

  • Update your snapshot.

  • Well, it tells us inspect your changes or run npm test --

  • -u to update it.

  • And so we can just run npm test --

  • -u.

  • And it will go ahead and update the snapshot in our test suite.

  • And so let's go ahead and revert our change.

  • And now again, if we test, what do we expect to happen?

  • Well, we changed something.

  • So the snapshot test should fail.

  • If we go read what failed, it says, oh, now this is minus.

  • It's gone.

  • And we expected it to be there.

  • We can that Jest know, oh, by the way, we meant for that to happen.

  • So -u.

  • Update your snapshots accordingly.

  • And now if we run npm test after updating,

  • everything passes, and all is good in the world.

  • Great.

  • So now let's add a few more tests just to make

  • sure everything's working as expected.

  • And so maybe we want to test the case where updateUser returns an action when

  • passed empty object.

  • And we can just cut and paste this.

  • And rather than passing an object here, we can just pass an empty object.

  • And we expect it to match whatever the snapshot was.

  • And so maybe we also want to test a case where you pass an empty name.

  • And so let's do name is empty.

  • And so now you're starting to see a bunch of very similar tests.

  • And maybe there's a better way to go ahead and group them together.

  • And it turns out there is.

  • You can describe a group of actions, and Jest

  • will automatically group them for you.

  • And so the way to do that is--

  • let's first look at the output of this.

  • It just lets us know that some tests are passing.

  • But maybe let's add a logical group of tests together.

  • So let's do actions.test.js and group these very similar tests together.

  • And so now let's describe a group of tests.

  • And so this one is updateUser returns actions.

  • And it takes a callback, just like the other tests.

  • And now we can group these things together.

  • And rather than using test, we do it.

  • And what does it do?

  • It returns an action, or it handles an empty object

  • or it handles an empty name.

  • And now if we run these tests, we see that there--

  • well, they have obsolete snapshots because we updated the tests.

  • So we'll go ahead and first update all of these things.

  • And now it works.

  • But it's not giving me the pretty output.

  • Let me see why not.

  • Oh, we should run our watch mode.

  • Hmm.

  • It's not giving us the pretty output.

  • And I will look into it at the break.

  • But this is a way of joining very similar tests

  • together in logical groups.

  • And we should let ESLint know that we added a few more globally available

  • variables, including describe and it.

  • Great.

  • So now let's take a look at trying to test a more complicated action.

  • So what happens if we want to test this async action that we wrote?

  • It's a lot more complicated than the simple actions

  • that just took a single argument and immediately returned a new object.

  • This one's doing a few different other things.

  • And so how might we go ahead and do this?

  • Well, one, it's async.

  • So that might be a bit of a difficulty.

  • So it turns out--

  • oh, so which of these functions should we use?

  • If you're using primitives, then you can use toBe

  • because as we learned in an earlier lecture,

  • you can just check primitives with triple equals.

  • And it'll go ahead and see if those values are equivalent.

  • If you're doing objects and you don't expect those objects to ever change,

  • you might want to use toEqual like we did in the earlier example

  • because it will do a deeper equality check.

  • But that has the downside that it will actually error

  • if we end up changing the objects.

  • And we'll have to go back and rewrite those tests.

  • If we have objects that might change in the future,

  • that's when we should use something like a snapshot

  • because then it will give us the benefits of knowing

  • that we changed our function.

  • But if that was a change that we were OK with or a change that we intended,

  • then we don't have to go back and rewrite all of our tests

  • like we would have if we were using that toEqual.

  • So now let's take a look at some asynchronous actions.

  • And so why might this be more difficult?

  • Well, it adds a few different difficulties, it seems.

  • So we have to wait for the results to come back

  • before we check against the results.

  • So that might be a little bit tricky.

  • Our tests also might rely on libraries, other libraries that we go ahead

  • and import.

  • And lastly, what about external services?

  • And so in our login user, we're actually awaiting

  • login, which sends a fetch request to an external service.

  • So how might we go ahead and combat these three difficulties?

  • Well, for the first one, if we return a promise,

  • Jest is smart enough to be able to wait for it to resolve

  • before it checks against that value.

  • Jest also supports async await, which is awesome

  • because we can just await a value, and then go and check against it.

  • And so let's go ahead and start to write a test for this asynchronous action

  • created here.

  • And so we're going to be using logInUser

  • And let's describe a group of tests to see if logInUser

  • returns the actions that we want it to.

  • So first, logInUser should do what?

  • It should dispatch an action that the login is sent.

  • Then it's going to try to do something by awaiting a login.

  • And then it's going to dispatch something else.

  • And then if there's an error anywhere up there,

  • it's going to dispatch something else again.

  • And so it might be a little bit difficult to track what's going on.

  • So let's first just write some dummy function dispatch

  • that doesn't actually do anything.

  • Then, if we wanted, we could await logInUser.

  • But what happens then?

  • If we run this, we'll see that--

  • uh-oh.

  • Await is a reserved word.

  • So what do we have to do if we're ever going to use that await key word?

  • Well, first we need to make sure to let JavaScript know that hey,

  • this is not a normal function.

  • This is an async function.

  • And so now we can invoke logInUser with whatever we want--

  • so the user name and password--

  • and then also pass it dispatch.

  • And then we expect it to error here.

  • And I'm just going to wave my hand at that case for now.

  • So right now, for this function called dispatch,

  • we don't really know what to do with this.

  • In our actual application, what happens in this particular action?

  • Well, we have that Redux [? flunk ?] middleware,

  • which handles passing that dispatch function in for us

  • and will dispatch those actions to the store on our behalf.

  • But in our particular unit test here, we don't really have access to that store.

  • We don't want to rely on these external libraries

  • in order to run this code here, which is that second difficulty here.

  • And so how might we go about that?

  • Right now, we just wrote a dummy function.

  • But certainly there's a better way to do this.

  • Well, it turns out Jest supports doing what are called mock functions, which

  • is similar to what we're doing, but these mock functions actually

  • do something.

  • They will track what they're invoked with or invoked on.

  • And so we can actually use this mock to be

  • able to figure out exactly what actions are being dispatched because if you

  • remember the way that this action works, first it'll

  • dispatch an action that's letting the Redux store know that we

  • have sent off this login request.

  • Then it's going to go ahead and do it.

  • And depending on what happens, it's going to dispatch other actions.

  • And so if we mock this dispatch function,

  • we can go ahead and check exactly what values the dispatch is invoked with.

  • So how might we do that?

  • So rather than-- oh, this should be actions.logInUser.

  • Rather than just writing dispatch as a dummy function that we wrote here,

  • let's actually use jest.function--

  • .fn, I believe-- which creates one of those Jest mock functions.

  • And so now we can go ahead and use that to track exactly what happens.

  • So let's do it.

  • So let's test that it dispatches the login sent action correctly.

  • And how are we going to test for that?

  • Well, first we're going to create that dummy function,

  • that mock function by Jest.

  • Then let's go ahead and kick off the action here.

  • And so let's just write mockDispatch so that we can remember what that is.

  • So great.

  • We created a mock dispatch function.

  • We go ahead and pass it in as the mock dispatch in our login actions.

  • And how are we going to check to see exactly what happened?

  • Well, it turns out attached to mock dispatch, we can see all of the things

  • that it was invoked on.

  • So first let me fix this.

  • This should be an async function.

  • And this no longer needs to be an async function.

  • So how might we want to check to see what the mock dispatch was called on?

  • Well, it turns out we can check.

  • We can see mockDispatch--

  • let me check the documentation really quick--

  • .mock.calls.

  • And what that does is it's all of the calls

  • that the mock function was invoked on.

  • So that's really helpful.

  • Why is that helpful?

  • Well, we can just see if that dispatch was invoked with the action

  • that we were expecting it to be invoked with.

  • So let's just console log that for now.

  • And let's also let ESLint know that jest is globally available.

  • So now let's run npm test.

  • And we see what?

  • We see that login was sent, which is great.

  • We expected dispatch to be invoked with login sent.

  • And then we also see that login is then later rejected because fetch is not

  • defined, which is a problem.

  • But for now, we're only focusing on whether or not

  • the login sent was done successfully.

  • So let's finish that test to actually check for that.

  • So rather than console.logging the mockDispatch.mock.calls,

  • we can actually expect it to be some value.

  • So let's expect mockDispatch.mock.calls.

  • We should probably look at the first time it was called,

  • which is indexing into that first array.

  • And let's also look at the first value or the first argument

  • that it was invoked with.

  • And so let's index into that.

  • And we expect it to be something, right?

  • We expect it to be an object that has a login sent value as the type.

  • And what's going to happen when we run this?

  • Does anybody know?

  • We get that same error that we got earlier.

  • We see that these two objects are the same.

  • But the test is still failing.

  • And it's the same reason why it failed earlier.

  • The two values have no visual difference,

  • but we are checking their reference.

  • And since we just created that second object,

  • obviously those references aren't going to match.

  • And so rather than using toBe, what should we do instead?

  • Well, we should probably use that .toEqual.

  • And now we see that those do, in fact, pass.

  • Great.

  • But that wasn't really the difficult part, right?

  • It's pretty easy to see in our code that that's exactly what's going to happen.

  • Really, the hard part is to make sure that this does what we expect it to do.

  • But since login is defined within the logInUser,

  • that means we have to rely on that to work.

  • And we don't really want our unit tests to rely

  • on things that are way outside our control, in this case

  • an API network request that's being sent out.

  • So how might we get around that problem?

  • It's actually not an easy one to solve.

  • There's actually a strategy called dependency injection,

  • which we can go ahead and use to get around this problem.

  • And so what dependency injection is is that we

  • pass functions on which other functions rely

  • on as arguments to those functions.

  • So we make these functions even more [? pure. ?]

  • And so everything that they need, they receive as arguments.

  • And how does that help us?

  • Well, it allows us to mock the functions that rely on external services, which

  • is pretty nifty.

  • So rather than using login here, we can make

  • this [? pure ?] by taking the login function

  • as an argument in our logInUser.

  • But what did we just do?

  • We changed the-- well, we didn't actually change anything yet.

  • If we use it here, now we've gone and changed

  • the way that you're supposed to use the logInUser function, which

  • means we broke backwards compatibility, which means every single time we use

  • this logInUser function in our entire app, we now have to go and fix,

  • which is not really fun.

  • So how might we get around that?

  • So what happens every time, currently in our app, we want to use logInUser?

  • Right now, we pass two arguments, username and password.

  • And in no places do we pass a third argument

  • because we weren't supposed to.

  • And so if we know that we're never going to pass a third argument,

  • we can have some sort of default argument that it falls back on.

  • And so one thing that we could do is we could

  • do const login or const realLoginFn is either

  • the login function if it's passed--

  • so if so.

  • Otherwise, the login that we imported at the top of the file here.

  • Or if we want to use some more shorthand, we can do login or login.

  • Or we can rely on a JavaScript feature called default function values.

  • So if nothing is passed as the argument, a third argument here,

  • we can have a default value be login, which

  • means expect to get a username and a password in the login function.

  • And if you receive undefined for this third value,

  • then use this default value instead.

  • And now we've added dependency injection,

  • but we haven't actually changed our logInUser function.

  • Because everywhere that we use it in our app, we pass two arguments.

  • And this login function will always fall back

  • to the default, which is the login function which we imported up here.

  • And so that will always be used here.

  • And so it does exactly what it did before.

  • But now in our tests, we can go ahead and add a mock function for login.

  • So let's go ahead and do that.

  • So now we have a test.test to see if it dispatches login sent.

  • We can add now a test with correct credentials.

  • So now let's make sure that if we pass the correct credentials, we get in.

  • We get the login fulfilled.

  • So how are we going to do that?

  • Well, we should probably have a mock dispatch.

  • But we need another mock function.

  • Or do we?

  • So the special thing about the Jest mock functions

  • here is that they can track whatever they were passed in.

  • And we can reference that later.

  • But does that really help us in our logInUser example?

  • Let's check back to see what happened here.

  • So for our login function down here, all that we're doing

  • is we're extracting a token from it.

  • And so either it works and we receive a token, or it doesn't work,

  • and supposedly an error is thrown, which we then catch down here.

  • And so we don't really need to mock a function using Jest here

  • because we don't really care what username and password were passed in.

  • And we don't care about the history going back.

  • We really only care about whether this login function returns a token,

  • or whether it throws an error.

  • And so rather than using the jest.function,

  • we don't need to use the mock function.

  • Instead, we can actually just define a function ourselves.

  • And so let's do that.

  • Let's do const login is a function that takes a username and a password.

  • And what happens?

  • So if the username is username, or we can just--

  • do whatever we want.

  • We can say if the user name is u and the password

  • is p, then what do we want to do?

  • We want to return a token.

  • So this is a test token.

  • Otherwise, what do you want to do?

  • Well, it should throw an error, right?

  • So we can throw new error.

  • Incorrect credentials.

  • So let's clean this up.

  • So now we have a mock dispatch function.

  • And we also have now a mock login function.

  • And when I say mock, it's not actually a Jest mock function.

  • It's just a function that we defined.

  • And so now we can actually test this.

  • We can say const token is await login username and passwords.

  • So rather than writing that out, let's just do expect that if we invoke

  • the actions.logInUser--

  • let's actually just do exactly what we did up here.

  • So let's await actions.logInUser.

  • Let's invoke with the username and password credentials

  • that we know are correct, u and p.

  • So let's do u here and p here.

  • And then what do we also have to pass into this function?

  • Well, we need to pass in the login function that it should be using.

  • And so we just wrote it here.

  • So let's also pass in the login.

  • And let's just call it mock login so we know it's a mock.

  • And then we also passing our mock dispatch.

  • And then what do we do?

  • Well, now what?

  • We expect that the mock dispatch has now dispatched more than one things.

  • One should be-- so mockDispatch.mock.calls.

  • So if we do the first one, what's not going to be?

  • What is the first thing that our logInUser action dispatches?

  • Well, if we refer to our actions.js file,

  • we see that always the first thing that gets dispatched is that login set.

  • But now for this particular test, since we're

  • testing that the login fulfilled action gets dispatched,

  • now we're interested in what's dispatched second.

  • And so let's go ahead and actually match against the second one.

  • And we can do .toEqual that action.

  • Or, what's better is we can just do to match the snapshot.

  • Well, maybe we should do both because for the first time,

  • we're not really guaranteed that it's actually

  • going to be what we want it to be.

  • So let's also expect that the first one is

  • going to equal the type of login filled with a payload that contains

  • a token, which is thisIsATestToken.

  • And now, assuming there are no syntax errors,

  • that should be what we expect it to be.

  • So let's go ahead and just run these tests.

  • And await is a reserved word.

  • What does that mean?

  • It means we forgot to use async.

  • So let's go ahead and make the second test also a async.

  • And now we can see whether or not our test passes.

  • It does not because it can't read property two of undefined.

  • Oh, we don't want two here.

  • We want one.

  • I was not zero indexing properly.

  • Because if we want to get the second value in an array, which array index do

  • we actually want to look into?

  • Probably one, right?

  • Great.

  • So now-- oh, there's also a bug.

  • We don't want the second.

  • We don't want the third array index of this.

  • We actually want array index zero.

  • And so now, hopefully this passes.

  • And we see an error.

  • It might just be the wrong shape.

  • Yeah, the payload is just the string rather

  • than being an object with a token in the string, which might actually

  • be a bug if we want to follow a very strict shape in our flux action.

  • But let's actually just write the tests to fit the function for now,

  • since we're already using that function successfully.

  • But our test actually just kind of caught a bug for us, which is cool.

  • So let's go ahead and just update our test so that it passes.

  • So rather than having the payload be an object with a key called token,

  • we just pass in the token as the payload.

  • And now we'll see that the test passes correctly.

  • Awesome.

  • So let's quickly handle our last case.

  • We want to ensure that we dispatch login rejected if we try to log in

  • with the incorrect credentials.

  • So we can move that mock login function outside so we can use it again.

  • We can also delete this if we wanted to since we

  • know that the snapshot is correct.

  • But it's not that expensive to run.

  • Let's just keep it in there for extra safety.

  • And let's just do it dispatches login rejected.

  • And let's just copy and paste the whole thing.

  • So we want it to dispatch login rejected with incorrect credentials.

  • And so now let's just pass in an empty string, an empty string.

  • We expect now the call not to be type actions login fulfilled,

  • but to be type login rejected.

  • And we want it to match the snapshot.

  • So let's just run the test.

  • Maybe we're passing in the error string, as well.

  • So the payload is incorrect creds because we're

  • passing in the error string.

  • So let's just update that really quick.

  • Because right now, we're only checking to ensure

  • that the first dispatch, or the array index one, or the second dispatch

  • has a type actions login rejected.

  • But it turns out we're also passing in the error message here as the payload.

  • So let's make sure that this is whatever we called up here.

  • And we can actually abstract that out.

  • So let's ensure that the payload is the error message.

  • And while we're at it, we can also not hard code the token here, but also

  • abstract that out.

  • So now we're guaranteed that whatever fake token

  • we return from the mock login on successful

  • is the exact string that we're checking against in the payload,

  • eventually, in our test.

  • So that's just abstracting it out so that we're not hard

  • coding things everywhere.

  • And so now let's run our tests.

  • And hopefully, every single one will pass.

  • Great.

  • So let's take a short break.

  • And when we come back, we'll see how to test some more complicated structures.

  • Hello, and welcome back.

  • So before the break, we were talking about testing

  • and how to test things like simple Redux actions,

  • and also things like asynchronous Redux actions.

  • And a great question came up during break.

  • And it was, hey, how come our tests look very similar to the functions

  • that we've implemented?

  • And is it really testing what it should be testing?

  • Because it's basically just implementing the function that we're testing.

  • And let me show you an explanation.

  • So in our particular logInUser function here,

  • there are really only two possible paths for our code to take.

  • Either the login succeeds and we fulfill that login, or the login

  • fails and the login is rejected.

  • And so you can think of it as a tree of the possible different ways

  • this code can happen, or just a tree of all of the possibilities.

  • And so there were really just two code paths.

  • Either first, you dispatch login sent, and then you dispatch login fulfilled.

  • Or you dispatch login sent, and then you dispatch login rejected.

  • So there are really only two branches.

  • And so in our tests, we're actually testing three different things.

  • We're testing first that we do what the first line here,

  • where we test whether login is sent.

  • And then we test both of the branches.

  • So either this branch is taken, and we fulfill the login,

  • or this branch is taken, and we reject the login.

  • And in this particular test here, we're testing

  • that bottom branch, whether or not we--

  • oops.

  • I need to scroll down a little bit.

  • So first we either test that the login is sent.

  • So we test the first line of code.

  • And then we test both possible branches.

  • We test either that login fulfilled is reached or login rejected is reached.

  • And so if we want to dig in a little bit into that login rejected,

  • we can see that although it's similar to the code that is written in login user,

  • it's not really reimplementing anything.

  • In login user what we're doing is we're first dispatching an action.

  • And then we're checking the result of our login function.

  • And so we're going to attempt to log in with the username and password.

  • And depending on how that goes, we're either

  • going to take the branch where we fulfill the login,

  • or we take the branch where we reject the login.

  • And so if you look at our logic in our test here,

  • first we create a mock dispatch.

  • We have already created a mock login.

  • But the first thing that we do is we execute some logic

  • where the code branches.

  • And so we try to log in the user using an empty string for both the

  • the username and the password.

  • So we're sending bogus information, hoping that the login gets rejected.

  • And so that's what we're testing down here.

  • Were saying, hey, ensure that the action that gets dispatched is of type login

  • rejected and has a payload of the error message.

  • And just as a sanity check, we're also going to do a match snapshot here.

  • And we can actually remove the first one once we know that the snapshot has

  • the correct action.

  • And so just to reiterate, even though the test

  • looks very similar in the way it's laid out to the function,

  • it actually does test it pretty well because we

  • have one test to do the first line.

  • Then we have two separate tests to test either branch of the possibility

  • where really the only logic lies, whether we login successfully

  • or unsuccessfully.

  • So, yeah, hopefully that addresses the question that came up.

  • And now let's forge ahead.

  • One more thing to note--

  • I was curious earlier why our tests weren't

  • being enumerated when we run npm test.

  • And it's because if you run npm test--

  • or if you run Jest with a flag called verbose,

  • that's what triggers all of the tests to be enumerated.

  • And so if we run with that --verbose flag, we then see all of the groupings

  • that we dictated in our test files.

  • And so we see in actions.test.js, we see the first describe block,

  • which describes the update user returning actions.

  • And then for each it block, we see those strings being repeated here.

  • And so the first it was it returns an action.

  • And then it handles an empty object.

  • And then it handles an empty name.

  • And we see that they all executed correct with that green check mark

  • there.

  • We see the second group here.

  • And we also see all of the tests that we specified in sum.test.js.

  • And so these were not grouped using a describe block like these ones.

  • They were just using that test function.

  • And so they show just as separate tests here.

  • And so if we wanted to show that entire enumeration of all the tests every time

  • we run npm test, then we need to change our package.json

  • to ensure that that flag is passed.

  • And so tests.

  • We should --verbose here and also here.

  • So now when you run npm test, we'll see that all of the tests

  • are enumerated for us.

  • Cool.

  • So we talked about a lot of different unit tests

  • and how we would test our actions.

  • And so now let's test something a little bit more involved.

  • So let's test our Redux reducers.

  • So just check our reducers the way that we implemented it before.

  • Let's just remove this to shut up ESLint.

  • So now there are no linting errors, we can

  • see that we have two different reducers here.

  • So one handles all of our contacts.

  • And so our contacts reducer takes the previous state and an action,

  • and depending on what the action does, it returns a new state.

  • So if the action's type is update contact,

  • we add a new contact into the state and return it.

  • And the way that we do that is we use this array spread.

  • So we basically clone the array from the state,

  • and then tack on at the and the action.payload.

  • If the action is not update contact, we just

  • return to state unchanged because we don't really have anything to change.

  • Our user reducer is a little more complicated

  • because there are different types that we want to check against.

  • If it's update user, then we merge the payload in.

  • If it's update contact, we merge this new object

  • where the previous contact is the payload.

  • And similar thing for login fulfilled or login rejected.

  • And if none of these match, then we just return the state unchanged.

  • And how are these exposed to Redux?

  • Well, we have our single reducer, which combines these two.

  • And so the user reducer is responsible for any changes in the user key.

  • And for any changes in the contacts key, it gets passed to that contact reducer.

  • So let's go ahead and create a few tests to make sure

  • that the reducer is doing what we want it to do.

  • So we do this in a reducer.test.js file.

  • The first thing that we want to do is let's disable ESLint

  • so it's easier to read.

  • And let's import the reducer.

  • And let's also import our actions.

  • Cool.

  • So let's just pick a couple things at random to test for.

  • In reality, we'd want to test all of our functionality in reducer.

  • But since we don't have time for all that,

  • let's just check a couple of things.

  • Let's just check contact reducer.

  • If we add a new contact, let's ensure that it makes it into our state.

  • So let's do describe the contact reducer.

  • And it successfully adds new user.

  • And how are we going to check it for that?

  • Well, first we should expect the result of passing

  • in user reducer or the reducer--

  • so the reducer takes a state and an action.

  • So let's just create a default state for now.

  • So let's have a default state just be--

  • let's have the user be an empty object and contacts be an empty array.

  • And I say we should pass the reducer in an initial state.

  • So let's pass the default state here.

  • And it should take an action, as well.

  • And so let's just pull in an action creator from our actions file.

  • And I believe it's called add user.

  • And let's add a user with a name of test user and a phone of that

  • and expect it to just match the snapshot--

  • to match snapshot.

  • This might actually be called add contact.

  • And just to make it a little bit more readable, do that.

  • And now let's also run our tests in watch mode

  • so we can see what happens as we write them.

  • And so we see that the test is written.

  • One snapshot written in one test suite.

  • So now let's change this to make sure the snapshot is what we want it to be.

  • So let's test user, exclamation point.

  • Save that.

  • The snapshot test should fail.

  • Let's go see why it failed.

  • Because our object that was our state now contains the contacts

  • where the array is test user with an exclamation point rather than without.

  • But it looks fine the way it was before.

  • So let's just revert.

  • And so now we know that this snapshot is what we want it to be.

  • Let's also describe the user reducer.

  • And that successfully updates user.

  • So let's expect the user, if we pass in the default state,

  • let's try to update the user.

  • Maybe that's called update user.

  • And just change the name to test user.

  • Let's see what the tests have to say.

  • One snapshot written.

  • We can see exactly what the snapshot is.

  • We can open up the snapshot file.

  • Or we can just change this really quick so that the diff shows.

  • We can see that now the snapshot test fails, as expected,

  • because now the user, the name is test user with an exclamation point.

  • It was fine the way it was before.

  • So let's just revert.

  • And now our test should pass because the snapshot matches the snapshot

  • that it was before.

  • And it does indeed pass.

  • Great.

  • So now we've gone and tested our reducer.

  • Obviously, there's a lot more to test.

  • But since we're running low on time, we'll just leave that untested for now.

  • So now we've basically tested everything in Redux.

  • But we're yet to actually touch any React or React Native.

  • And so now let's look at some integration tests.

  • And so how might we want to go about testing React components?

  • Well, we can use Jest snapshotting.

  • Just like we were snapshotting and looking at any diffs

  • between what changed in the output of the reducers or any of the actions,

  • we can also use the snapshot feature to test the output of React component

  • rendering.

  • And so how are we going to render React outside

  • of the context of our application?

  • Well, it turns out React allows us to do that

  • by using this React test renderer, which is maintained by the React team.

  • It allows us to just render components outside the context of an application.

  • And so the docs are here if you want to look at them.

  • But we'll be using that in the demo.

  • One additional thing is that now we're going

  • to need to configure Jest a little bit more so that it worked with React.

  • It turns out the Expo team has done this for us.

  • Jest Expo has all the configuration we need.

  • And we can just look at the docs here to see how to use that.

  • So let's just take a quick peek.

  • We can see that we need to run npm install jest expo and save dev.

  • So let's go ahead and do that.

  • And while it installs, we can also see that we

  • need to add a little bit to our package.json.

  • We already have a test script.

  • But we need to add a Jest key that let's Jest know to use the configuration

  • preset specified within Jest Expo.

  • So let's just cut and paste this into our package.json.

  • So we can now open up package.json and just add our Jest preset.

  • So now in theory, we are all ready to try our integration tests.

  • And so now let's actually try this thing called test-driven development.

  • And so there's another question that came up during break that was,

  • hey, it looks like we wrote our entire app.

  • And now we're writing test after the fact.

  • Is that what we should be doing?

  • It turns out there's actually a strategy to writing tests and writing code.

  • And it's called test-driven development, whereby the tests drive

  • what you should be implementing.

  • And as long as you know exactly what you want to be implementing,

  • you can write the tests first, and then implement

  • the components or whatever you're implementing such

  • that it fulfills all of the tests.

  • And so the tests are less tests and more the specification.

  • Or in other words, what should this function,

  • or what should this component, be doing?

  • And so let's actually write a simple button component and use

  • test-driven development to do so.

  • So let's just make a components directory.

  • And let's actually write the test file first.

  • So let's do MyButton.test.js.

  • And shut up ESLint for now.

  • And first we're going to want to import MyButton from a file

  • that we haven't yet written.

  • But it'll be eventually written in MyButton.

  • And what do you want to do?

  • Well, first we should describe it.

  • So let's see what happens in MyButton.

  • Well, first, it should render.

  • It should just appear.

  • So let's just test that it renders.

  • And so now we've run into a little bit of difficulty.

  • How do we ensure that it's rendering?

  • So this is actually what React test renderer does.

  • It allows us to render these components outside the app

  • to make sure they actually exist.

  • So let's go ahead and do that.

  • So let's do import render from react-test-render.

  • And we can do const button is render.create.

  • And what do we want to create?

  • Well, we just want to do a MyButton.

  • And maybe we should turn into JSON so it's easy to match against.

  • Just do .toJSON.

  • And then let's just make sure that it matches

  • the snapshot that it was before.

  • So let's do expect button to match the snapshot.

  • And now let's run our tests in watch mode

  • so we can actually implement the button as we go.

  • So it looks like it's erroring.

  • The test failed.

  • Why did it fail?

  • Well, obviously, it can't find my button because we haven't even

  • written the file yet.

  • So let's go ahead and do that.

  • So let's open up MyButton.js.

  • And let's just do export default empty.

  • Just an empty function.

  • We'll save that.

  • We'll see what the tests have to say.

  • OK.

  • Now a different error.

  • Renders, it failed.

  • React isn't defined.

  • Maybe we should import React from React.

  • So in both these files, let's do import React from React.

  • And now the tests run again.

  • And another one fails.

  • What happened here?

  • Well, it didn't actually render.

  • Invariant violation.

  • We wanted a component, but nothing was returned from render.

  • Well, great.

  • Our tests caught an error.

  • So it's not actually rendering anything.

  • So now let's actually render a button.

  • So let's import button from react-native.

  • And now we can do just return a button.

  • And now we're seeing a test failure.

  • We can go up.

  • And we can see, oh, this failed because we're not using button correctly.

  • We need to pass a title to button.

  • OK, so our test's still failing.

  • Let's go back and fix that.

  • So we can say the title should be, I don't know, test button.

  • And maybe let's also get ahead and pass on press as some empty function

  • because that's also something that's required.

  • And now look it.

  • It passed.

  • Yay.

  • Our first test, which is just saying the button should render, it now passed.

  • The button did in fact render.

  • And so now let's try to add one more feature.

  • Let's it correctly overrides color, the default color.

  • So what we want to happen is we want to be

  • able to pass a button color as a prop to the button

  • and make sure that it overwrites any default.

  • So let's do const color is red.

  • And const button is the render.create that.

  • And rather than just immediately JSONifying it,

  • let's actually grab the root component.

  • Let's grab the button out of it.

  • So let's just do .root.

  • And so if you look at the React test render docs,

  • it says you can get the instance of a class or a component by just doing

  • .root.

  • And you'll get the outer one.

  • Now we want to check the props to make sure that the color matches

  • the color that we specified.

  • And so we can do expect button.props.color.toBe--

  • and what do we want it to be?

  • Well, the color that we specified.

  • And maybe here we should pass in the color is the color.

  • So now let's run this and expect it to fail, which it didn't.

  • Interesting.

  • Let me make sure I'm using it correctly.

  • I'm surprised it worked because it shouldn't be.

  • Let's see.

  • My button color color.

  • We passed color here.

  • It doesn't get set.

  • So this is supposedly undefined.

  • Let me just console.log the button.

  • Let me console.log the button's props and ensure that the color

  • is what we expect it to be.

  • So it's receiving color red, which is interesting.

  • Oh, because we don't want the root.

  • We actually want the button part of it.

  • So the reason that it's failing is because we're not

  • grabbing the correct part, the correct component here.

  • But since we're running low on time, I will just write it up at home

  • and post it.

  • But let me just move on so that we can get to the rest of the lecture first.

  • And so assuming that this code works, we can

  • see that by writing the tests before we write the code,

  • we can start to write a spec that allows us to then implement whatever component

  • or function we want to implement the spec that we described.

  • And so we can use integration tests like these snapshot tests here

  • to ensure that our app retains all of its features

  • and we don't break any features as we start to add new things.

  • And so how do we then--

  • how can we tell how much of our app is actually being tested?

  • Because right now, we can kind of remember, oh, we tested Redux.

  • We tested this button.

  • We haven't really touched the app.

  • We haven't touched any screens.

  • So we can kind of keep track in our head.

  • But there's no real way for us right now to know exactly how we do that.

  • And it turns out there's actually a metric.

  • It's called code coverage.

  • And so this is a metric for tracking how well-tested an application is.

  • And there is a few different numbers.

  • One is statements, or in other words, how many statements in this program

  • have been executed?

  • Another is branches.

  • And so just like we were discussing before

  • with a couple of different possible code paths in our reducer or action,

  • this is the metric for that.

  • It's how many of the possible code paths in all of our application

  • have been executed or have been tested?

  • Another one is functions, meaning we have n functions in our app.

  • How many of them are actually tested?

  • And lastly, just straight lines of code.

  • So out of all of the lines of code in our app, how many

  • have actually been executed by our tests?

  • And so how do we get this coverage report?

  • Well, it turns out we just pass another flag to Jest.

  • We can just pass --coverage, and it will let us know all of these numbers.

  • And so we can do npm test --

  • --coverage to pass that in.

  • And it will run all of our tests and at the very end output a table.

  • And if I zoom out so that it formats correctly,

  • we can see all of the numbers here.

  • So we can see all of the files that we've tested.

  • And so we tested api.js.

  • We tested MyButton.js.

  • We tested actions.

  • We tested reducer.

  • And in here, it tells us exactly how many of each metric

  • have we actually tested.

  • And so in sum.js, if you remember, it was just

  • a single function that returned with the sum of two numbers.

  • It turns out with the tests that we wrote, we hit 100% of the statements,

  • 100% of the branches, 100% of the functions, and every single line,

  • as well.

  • But for the things less tested, like our reducer, just like I mentioned,

  • oh, we're going to not test the sections of the reducer.

  • We see that only 85.71% of the statements

  • are tested, which equates to 78% of the branches.

  • But every single function was tested because if you remember,

  • there was just the user reducer, the context reducer, and the reducer,

  • and maybe a merge function.

  • But we ended up invoking every single function, so 100% of those are tested.

  • But we only ended up touching 83.33% of the lines of code.

  • We can see that lines 19, 20, and 21 were not actually tested.

  • And then for the things that we really didn't test at all,

  • we see some very low numbers.

  • And so that is the code coverage report for the tests that we just wrote.

  • And so we've talked about unit tests and we've talked about integration tests.

  • But what about end-to-end tests?

  • And so unfortunately, currently there's no real easy way

  • to run automated end-to-end tests in React Native.

  • But there's a great work in progress by Wix.

  • And so the company Wix is writing this library

  • called Detox which actually handles end-to-end testing in React Native.

  • If you want to check it out, the github link is here.

  • There's also a github link where Brent started putting together integration

  • between Expo and Wix Detox tests.

  • But right now, it really lacks Android support.

  • And so it's not quite ready to test your apps.

  • But I encourage you to follow along with this project

  • just because it allows you to do really cool things like this.

  • And so you see here it's automatically downloading a bunch of stuff.

  • It's going to run the app.

  • And it's going to just zip through the app testing all of your features,

  • as if it were a user running the application.

  • And so this is truly end-to-end because it basically

  • simulates an end user lifting up the app and testing all of the functionality

  • very deeply.

  • And so everything, all the code that executes in order

  • to do a certain feature is executed by this Detox application.

  • And lastly, I just wanted to give some thanks

  • to everybody who's been helping out with this course,

  • to the CS50 team, including David, the production team,

  • and everybody else who just made this possible.

  • And then good luck on your final projects.

  • I'm really excited to see what you all build.

  • And thank you for joining us for this semester.

[MUSIC PLAYING]

Subtitles and vocabulary

Click the word to look it up Click the word to find further inforamtion about it