字幕列表 影片播放 列印英文字幕 [MUSIC PLAYING] JONATHAN GERRISH: Hello, everyone. Welcome to this morning session on test-driven development for the Android platform. My name is Jonathan Gerrish and I'm part of the Mobile Ninjas. We're a small team within Google who are passionate about software testing. Can I get a quick show of hands in the audience? How many of you are actually writing tests as part of your normal software development practice? That's fantastic. OK. So if you've written tests for Android before, you've probably used some of our tools. We developed the Android testing support library, which includes the JUnit for test runner and rules, the Espresso UI testing framework, and we're also active contributors to Roboelectric, the open source Android unit testing framework. So everyone is telling you to write tests, but why should you really do it? It's true that tests take time to write. They're adding code to your code base. And perhaps you've been in this situation before, where your manager or client has been telling you that they're slowing you down. But there's so many compelling reasons to write tests. Tests give you rapid feedback on failures. So failures that are spotted earlier on in the development cycle are far easier to fix than ones that have gone live. Secondly, tests give you a safety net. With a good suite of tests, you're free to refactor, clean up, and optimize your code, safe in the knowledge that you're not going to break existing behavior. Tests are really the backbone of sustainable software development. You'll be able to maintain a stable velocity throughout the lifetime of your project, and you're going to avoid the boom-bust cycles of crunch feature time and the aggregation of technical debt. So in software testing, there exists the concept of the testing pyramid. And this is made up of a number of layers. And each layer brings with it its own trade-offs that you're going to have to weigh. At the lowest layer is the small tests, or the unit tests. And these need to be very fast and highly focused. That's why we recommend you run these kind of tests, what is known as local unit tests. And these are going to run on your local desktop machine. The trade-off you're making with these kind of tests is infidelity because you're not running on a realistic environment and you're probably substituting in a bunch of mocks and fakes. As we move up the pyramid, we're now into the realms of integration testing and end-to-end testing. And the key with these kind of tests is to bring in fidelity. That's why we recommend that you run these kinds of tests on a real device or an emulator. These are the kinds of tests that are going to tell you that your software actually works. However, they are less focused, so a failure in one of these kind of tests might take a little longer to track down than it would in a unit test. And one of the big trade-offs you're making is in test execution speed. Because you're assembling multiple components, they all have to be built and then packaged, shipped to a device where the tests are run, and the results are collected back. That's going to take extra time. There's no single layer in this testing pyramid that can suffice, so what you need to do is to blend in tests at each different tier, leveraging the strengths of one category to weigh off the trade-offs in another. There's no real hard and fast rule here, but Google's own internal testing experts recommend the 70-20-10 rule of thumb as the ratio between small, medium, and large tests. Let's take a look at our workflow. So with test-driven development, the idea is that you start by writing your tests, then you implement the code to make those tests pass. And then when your tests are green, you can submit. Again, a quick show of hands. Who out there has test-driven their code, tried test-driven development in the past? OK. Cool. We like test-driven development because it makes you think about the design of your application up front. It gives due consideration to APIs and the structure of your code. With test-driven development, you're also going to be writing less code because you only write the code necessary to satisfy your tests. This will enable you to release early and often. As you're constantly green, you'll be able to deploy a working application at a moment's notice. If you're following the test pyramid, the workflow is going to look something like this. First of all, we have a larger outer iteration that's concerned with feature development. Here, it's driven by a UI test, and the mantra with test-driven development is Red, Green, Refactor. We start off with a failing test, we implement the code to make that test pass, and then we refactor. Inside the larger iteration are a series of smaller iterations and these are concerned with the unit tests. Here, you're building the units required to make the feature pass. And again, you use the same mantra here. Red, Green, Refactor. Red, Green, Refactor. Let's take a look at an example application. The feature we're going to implement today is the Add Notes flow to a sample note-taking application. If we take a look at our mock-ups, we can see that we start on a notes list screen full of some existing notes. There's a floating action button down at the bottom. And the user will click this, taking them onto the new add notes screen. Here, they can enter a title and a description for their note before clicking Save. The note will be persisted and then they'll return back to their notes list screen, where they can see their newly added note, along with any other notes that previously existed. Coming back to our workflow for a moment, remember that we start with a failing UI test. So let's take a look at how this test would look using Espresso, the UI testing framework. The first step is to click on the Add Note button. Then we enter the title and description and click Save before returning to the notes list screen. And here, we're going to verify that the note that we just added actually shows up. Now remember, with test-driven development, we'll not implemented code just yet. All we have to do is implement enough of the application to satisfy the specification of our tests. So an empty activity, and just the resources that we need, will suffice. Once we have that, we can run our test and we'll see it'll fail. Now we have to implement this feature. So applications are built up of many small units. These are small, highly focused, specialized components that do one thing and they do it well. Collections of these small units are then assembled together so that their collaborations will satisfy our feature. Let's take a moment to summarize the key characteristics that make up a good unit test. As well as the normal conditions, you're wanting to test your failure conditions, invalid inputs, and boundary conditions. You're going to end up writing a lot of unit tests. Unit tests must always give you the same result every time. So avoid depending on things that might change-- For example, an external server or the current time of day-- because this is going to bring flakiness into your unit tests. Unit tests should exercise one specific aspect of your code at a time. You're wanting to see that a failure in a unit test will lead you, very quickly, to a natural bug in your code. And when you write unit tests, avoid making too many assumptions on the actual implementation of your code. You want your unit test to test behavior. That way, you avoid rewriting your test when your implementation is changing. And one of the most important aspects of unit tests is they've got to be fast, especially because you're writing so many of them and, during TDD workflow, running them rapidly. It would be terrible if you were discouraged from writing tests or refactoring your code because of the pain in the execution time of those tests. And finally, unit tests are an excellent source of documentation and the way it's constantly evolving with the code as it changes, unlike static documents that will stagnate over time. Let's try a unit test for our Add Notes activity. This activity is going to take in user input and then we're going to persist it to local storage on the device. OK. So we're going to create the Add Note activity class, and this will extend Activity, which is an Android framework class. It has a view which is going to be inflated with a layout. The user will enter their data here. And then we're going to persist that note into Android SharedPreferences mechanism. It's conceivable that, as our application evolves, so did our requirement. And perhaps our storage requirements evolve to persist the notes onto cloud storage and we have to build some kind of a synchronization mechanism for local storage for the offline use case. And in these cases, we see opportunities for abstraction. We might, in this example, see that we can extract a notes repository. However, one of the key aspects of test-driven development is that we only start by writing the simplest case first, and then we iterate. So we're going to resist the temptation to do this early. Let's take a look at a sample of what an idealized unit test would look like. They're generally built up into three stages. The first stage is you're setting the conditions for the test, and this includes preparing the environment, setting up your dependencies with their required state, and preparing any input data. Next, we'll exercise the code under test, before finally, making assertions on the results or the state. I like to clearly separate each of these three stages of the test and bring the pertinent aspects of each test front and center to make for a really readable test. Up until now with the Android platform, you're writing your unit tests using the mockable jarring conjunction with a mocking library, such as Marketo. And let's take a look at an example of a test written with Marketo. OK. Wow. That's a lot of code. OK. So because we have so many interactions with the Android framework, we're going to need to provide stubbing behavior for all of them in order just to make-- just to satisfy the execution paths of our test. And furthermore, because Android uses a lot of static methods, we're forced to introduce a second mocking library, PowerMock, that will handle this special case for us. And there are also some pretty bad code [INAUDIBLE] here. Let's take a look. You see, we're forced to spy on the activity on the test and we're needing to do this to modify its behavior. And stubbing it out and providing some no ops. So we're moving out of the realms of black box testing here. And finally, at the end, we're making assertions about the implementation details. And if these change, our test will need to change, too. So remembering the characteristics of a good unit test, let's take a moment to score this particular test. While it is very focused, we're just testing the happy path of our Add Notes flow, and it's certainly fast because it's running on the local JVM. However, we were making rather a lot of assumptions about the implementation in that test. And with this, if any of our implementation changes, it's likely we'll need to rewrite that test substantially. And finally, all that excess boilerplate stubbing is really distracting. It's distracting away from the key aspects of the test, the conditions of the test that you're trying to document. Well luckily, there's a tool that helps address some of these issues. So introducing Roboelectric. Roboelectric is an Android unit testing tool that's open sourced that we are actively contributing to. And to tell you more about how you can write great tests with Roboelectric, I'm going to hand you over to Christian Williams, the original author of Roboelectric. [APPLAUSE] CHRISTIAN WILLIAMS: Thanks, Jonathan. It's awesome to see so many people who are into Android testing and TDD. Yeah, Roboelectric is this scrappy little open source project that I started hacking on back in the early days of Android Testing because I was just super annoyed at how long it took to deploy and run tests on an emulator. And it's kind of been a side project of a bunch of different people until last year, when I had the privilege of joining my friend Jonathan at Google, where he was already working, on improving Roboelectric for Google's own internal test suites. And since then, we've been really beefing up Roboelectric and contributing back to the open source project. Today, Roboelectric isn't an officially supported part of the Android testing platform, but we found that, when it's used correctly, it can be a really useful part of your testing strategy. And I'm going to show you a little bit about how you can do that, too. Let's go back to our notes unit test and see how we might approach it with Roboelectric. Since Roboelectric runs as a local unit test, it'll still be running on your workstation, not on an emulator. But Roboelectric provides a little Android sandbox next to your test, where the actual SDK code is running. You'll have access to your activities, your layouts, and views, and resources. And you can generally just call most Android methods and they'll kind of work like you'd expect. There are parts of the Android framework that rely on native code or collective hardware or interact with external system services. So for that, Roboelectric provides a sort of test stubble that we call Shadows. And those provide alternative limitations of that code that's appropriate for unit testing. Remember that test that we just saw that had 20 lines of code, of mock set-up code? Let's see how that looks in Roboelectric. That's a lot less. We've gotten rid of all the boilerplate. The test is about half the size and much more concise. We're not forced to think about the implementation details as we're writing the test, which is quite nice. Roboelectric is going to set up the application according to your manifest. And if we were asking it to set up our activity, it runs it through the appropriate life cycle to get it into the right state. Inflates views, all that stuff that we expect on a device. So we can just interact with it as if you're on a device. So we add some text to some fields, click on it, and assert that it adds a note to the repository. Now, notice that we're not actually going as far as the UI test that we wrote at the very beginning. We're not asserting that the new note appears on the view screen. That would be the job of another unit test. Now, I mentioned Roboelectric's shadows. They actually give extended testing APIs to some Android classes that let us query internal state and sometimes change their behavior. In this example, we were asking the application if any of our activities requested that an intent be launched during the test. We could use that to assert that, after saving the note to the repository, we're going to go to the View Notes activity. Similar testing APIs exist for simulating hardware responses or external services, things like that. At this point, we have a failing unit test. And now we get to-- we're ready for the easy part, writing the production code. In the spirit of TDD, we're only going to write exactly as much as is needed to make the test pass. No more, no speculative coding. So we inflate a layout, attach a click handler, and when the click happens, we fade a note and add it to the repository. Now we can run the test, see it pass. If there's some improvement we can make to the code, we'll go back and refactor, and then we repeat. This is where you get the thoroughness. And Roboelectric is super handy for this because it gives you nice, fast test runs. You can get into a comfy cycle. We want to not just test the happy path here. We're going to test all the different cases that our code is likely to encounter. So for example, input validation and external conditions like the network being down and stuff like that. Roboelectric can also help with simulating device conditions that you'll encounter. For example, you can specify qualifiers that the test should run with. Here, we're saying a certain screen size and orientation, which might change the layout a bit. You can ask Roboelectric to run your test under a specific SDK. So we'll say Jelly Bean here. And it actually uses of the SDK code from that version. And you can also tell Roboelectric, I want to run this test under every SDK that you support, or some range of them that you're interested in. And we support Jelly Bean through O right now. At Google, we rely really heavily on Roboelectric and we're investing in making it better. We've got dozens of apps, including these, that have hundreds of thousands of unit tests running internally. So it's well battle-tested. And we've also recently started running the Android CTS, which is the official Android test suite against Roboelectric. And we're about 70% passing right now, getting better with every release. So if you used Roboelectric in the past and found that it's come up short, or if you're stuck in an old version, I definitely recommend that you get up to the latest because it's come a long way. We've been working on reducing friction in integrating Roboelectric with the Android tool chain. It works now very well with Android Studio, with Gradle. And we've got support for Bazel, Google's own open source build system coming soon. Roboelectric isn't a one-size-fits-all testing tool. It's fast, but it's not 100% identical to Android in every way, so you want to use it judiciously. As I said before, avoid writing unit tests that link multiple activities together. That's not so much a unit test. That's much better for Espresso. If you find yourself dealing with multiple threads, synchronization issues, stuff like that, you're also probably not writing a unit tests, so not good for electric. And particularly, avoid using Roboelectric to test your integration with Android APIs and things like Google Play services. You really need to have higher-level tests to give you confidence that that's working. So now that we've got some passing unit tests, I'm going to hand you over to my colleague, Stefan to talk about higher level testing. [APPLAUSE] STEFAN: Thank you, Christian. Let's go back to our developer workflow diagram. At this point, we hopefully have a ton of unit tests and they thoroughly test all our business logic. But let's switch gears and try to see how we can actually write some integration tests to see how these units integrate, and how they actually integrate with Android and how they run in a real environment. On Android, these tests are usually referred to as instrumentation tests. And I'm pretty sure most of you have written an instrumentation test before. And even though they look super simple on the surface, there's actually a lot going on under the hood, if you think about it. You have to compile the code, you have to process your resources, you have to bring up a full system image and then run your test. And there's a lot of things that go on on various levels of the Android stack. So these tests give you high fidelity, but as John was mentioning, they come at a cost, which is they are slower and sometimes, they're more flaky than unit tests. So let's actually see how this works in your day-to-day development flow. Let's say you're an Android Studio. You've just written your new Espresso test and you hit the Run button to run the test. So the first thing that Android Studio is going to do is it's going to install two APKs for you, the test APK and the app on your test. Now, the test APK contains Android JUnit Runner, it contains the test cases, and your test manifest. And then, in order to run the test, Android Studio calls, under the hood, ADB Shell AM Instrument. And then Android JUnit Runner will use instrumentation to control your app on your test. What is instrumentation? I think you guys may have noticed this. It's a top-level tag in your manifest, and why is that? Instrumentation is actually something that's used deeply inside the Android framework, and it's used to control the lifecycle of your activities, for instance. So if you think about it, it's a perfect interception point that we can use to inject the test runner. And that's why Android JUnit Runner is nothing more or less than instrumentation. Let's go a little bit deeper and see what happens when Android Studio actually runs your test. It runs ADB Shell AM Instrument, which will end up calling out to Activity Manager. Activity manager will then call, at one point, onCreate on your instrumentation. Now that we know that Android JUnit Runner is our instrumentation, at this point, it will call onCreate on the runner. And then the runner is going to do a few things for you. It's going to collect all your tests. Then it's going to run all these tests sequentially and then it's reporting back the results. One thing to note here is that Android JUnit runner-- and you may have noticed this-- runs in the same process than your application. And more importantly, if you usually use Android JUnit Runner, it runs all the tests in one single instrumentation invocation. Android JUnit runner is heavily used inside Google. We run billions of tests each month using Android JUnit runner. And while doing so, we saw some challenges that we faced and that we had to solve. One thing that we see a lot is Shared State. And I'm not talking about the kind of shared state that you control and that you code in your app. I'm talking about the shared state that builds up on memory, builds up on disk, and makes your tests fail for no reason or unpredictable conditions. And this, among other things, will, at one point, lead to crashes. But in the previous module that I just showed you, if one of your tests crashes your instrumentation, it will take the whole app process with it and all the subsequent tests will not run anymore. And this is obviously a problem for large test suites. Similarly, if you think about debugging, if you run a couple of thousand tests in one invocation, just think about what your lock head will look like when you have to go through it for debugging. So that's why inside of Google, we have taken a different approach. Inside of Google, every test method runs in its own instrumentation and location. Now, you can do this today, right? You can make multiple ADB calls. You can use a runner arc and maintain your custom script. But the problem is it might not really integrate well with your development environment. That's why, today, I'm happy to announce the Android Test Orchestrator. And the Android Test Orchestrator is a way that allows you to run tests like we do in Google. It's a service APK that runs in the background and runs its test in a single instrumentation invocation. And this, obviously, has benefits, right? There's no shared state anymore. And in fact, the Android Test Orchestrator runs PM clear before it runs its tests. More so, crashes are now completely isolated because we have single instrumentation invocations. If a crash happens, all the subsequent tests will still run. And similarly, for debugging, all the debugging information that you collect and pull off the device is now scoped to a particular test. This is great and we benefit a lot from it inside of Google. Let's see how it actually works. On top of installing the test APK and on our tests, what we do now is we install a third APK on our device, and it's a service APK running in the background containing the orchestrator. And then, instead of running multiple ATB commands, we run a single ATB command. But we don't instrument the app under test. We instrument the orchestrator directly. And then the orchestrator is going to do all its work on the device. So it's going to use Android JUnit Runner to collect your tests, but then it's going to run each of those tests in its own invocation. And it's amazing and I'm pretty sure you will like this a lot. And it will be available in the next Android Testing Support library release. And more importantly, we will have integration with Android Studio. It will be available in Gradle and we will also have integration with Firebase Test Lab coming later this year. Now that we know how to run our test, let's actually look at how we can write these integration tests. And usually, if you write a [INAUDIBLE] test on Android, you're using the Espresso testing framework. And as you can see, espresso has this nice and simple API. And it actually works pretty simple. What it does is you give us a view matcher and we find a view in the hierarchy that matches that matcher. And then, we either perform a view action or check a view assertion. And because this API is so simple, it's the perfect tool, too, for fast TDD prototyping of UI tests. But in order to provide you such a simple API, there's a lot of things that need to go on under the hood. So let's actually look at how Espresso works. So when you call onView and give us your matcher, the first thing that we're going to do is we're going to create a view interaction for you. And then the next thing is we make sure that your app is in an idling, sane state before we are ready to interact with it. And you can think of it, this is at the core of Espresso. And Espresso is well-known for its synchronization guarantees. And the way we do it is we loop the message queue until there are no messages for a reasonable amount of time. We look at all your idling resources and make sure they're idle. And we also look at Async Tasks to make sure there's no background work running. And only if we know that your app is in a sane and stable state and we're ready to interact, we're going to move on. And then we're going to traverse the view hierarchy and find the view that matches your matcher. And once we have the view, we're then going to perform a view action or a view assertion. And this is great. So now let's circle back to the test that we showed you in the beginning and have a closer look now that we know how Espresso works. So in the first line, as you may remember, we tried to click on the Add Note button. And here, we're just going to use a with ID matcher, which is a simple matcher that is matching a view in the view hierarchy according to its ID. The next thing we want to do is we want to click on the View and we use a Click View action for this. Now, where it gets interesting is the next line. Because on this line, we want to type the title and description. And we want to use a type text action for that. But here, all the espresso synchronization guarantees will kick in and only if we know that we are ready to interact with your application, we're going to invoke the type test action. And this is great because it frees you from adding additional boilerplate code and additional slipping code to your test. So similarly, we're going to save the note and then we're going to verify that it's displayed on screen. And this is great. Now we know how Espresso works and we know how it's a great tool to do test-driven development. And now I'm going to hand it over to Nick to talk a little bit more on how you can improve your UI tests and how to improve your large and medium testing strategy. [APPLAUSE] NICK KOROSTELEV: Thank you, Stephan. So One good attribute of a UI test is a test that never sleeps. So let's go back to our example to illustrate this point a little bit further. In our example, as you remember, we have a note that we save into memory, which is pretty fast and pretty reliable. However, in reality, as your app grows, you probably want extend this functionality and save your note to the cloud or Google Drive, for example. So when running our large end-to-end test, we want to use a real environment where we hit the real server. And depending on your network connection, this may take a long time, so you probably want to do is in the background. Now the problem is that Espresso synchronization is not aware of any of your long-running tasks. This is somewhere where developers would probably do something as ugly as putting a thread sleep in their code. But with Espresso, it is not actually required because you can write an Idling Resource, where an idling resource is a simple interface for you as a developer to implement to teach Espresso synchronization of any of your custom, long-running tasks of your app. So with this Idling Resource, we made our large end-to-end test more reliable. So let's see how we can add some more medium-sized tests to your test suite. So for a medium-sized test, we want to keep them small and focused on a single UI component, where a single UI component may be a specific view, fragment, or an activity. So let's go back to our example to see how we can isolate our large end-to-end to more isolated components. Here in this example, again, you may have noticed that there are two activities. The List activity on the left and the Add Note activity on the right. So until now, we wrote a large end-to-end test that gives us a lot of confidence because it touches upon a lot of your code in your app, which is great for large end-to-end tests, but it's not so great for an iterative test-driven development cycle. So let's see how we can isolate these and have isolated tests for each activity in isolation. To isolate the left-hand side, the List activity, we can use Espresso Intent, where Espresso Intent is a simple API that allows you to intercept any of your ongoing intents, verify their content, and provide back a mock activity result. Great. Let's see how that API actually looks like. So as you can see, it's very straightforward. You have an intent matcher that will match your growing intent, and you can provide a version of your activity result back to the caller. OK. Let's use this API to write our first isolated test. In this test, you can see, on the first line, we do exactly that. We intercept our content and we provide a stub version of our activity result. Now, on the second line, when we perform Click, instead of starting a new activity, Espresso will intercept this intent and provide a Stub Activity result, which we can then use on the last line to verify that our UI was updated accordingly. Now we have an isolated test. OK. So let's go back to our example and see how we can isolate the second part, right? So when you usually write tests, you end up in a position where you may have some external dependencies in play that are outside of your control. In our example, as I showed before, we have a note that we save and it hits the real server. Now even though we have another resource now that makes it more reliable, your test can still fail because your server may crash for some reason. So your task will fail. So wouldn't it be better if we completely isolate ourselves from these conditions and run our tests in a hermetic environment? This will not only make your test run much faster, but it will also eliminate any flakiness. And beyond this specific example, you further want to isolate yourself from any external dependencies. So for example, you don't want to test any Android system UI or any other UI components that you don't own because they probably already tested and they can also change without your knowing so your test will actually fail. Let's see how our second isolated test will look in code. So the main point here is that we no longer use the real server. Instead, we set up a hermetic repository. Now, there's many different ways of you to do this and this is just one way. So then you can use this hermetic repository in order to verify that your note is actually saved without ever leaving the context of your app or hitting the network. So at this point, if you think about it, you have two smaller tests that are way more reliable and run much faster. But at the same time, you maintain the same amount of test coverage as your large end-to-end test. And this is why we want to have more of these smaller isolated tests compared to the large end-to-end tests we showed before. OK. So at this point, we iterated through our developer cycle a few times and we should see all of our tests start turning green and we should be confident to release our feature. However, before we conclude, let's jump into the future for a second. As your app grows and your team grows, you continue adding more and more features to your app. And you may find yourself in a position where you may have UI running in multiple processes, which is exactly what happened at Google. So if you go to our Add Notes example, this may look something like this. You have a first activity that runs in your main process on the left-hand side. And now the second activity will run in a private process. And in this case, we're going to call it Add Notes. So how do we test that? Well, before Android O, it wasn't possible to test. But with Android O, there is a new instrumentation attribute that you can use in order to define which process you want to instrument. While instrumenting and running tests, I guess, each process, in isolation, is a great idea and you should do it, you may find yourself in a position where you want to cross-process boundaries within one test. So you would probably want to write an Espresso test that looks like this. While this was not only impossible on a framework level before Android O, this was also impossible on Espresso level. Because in this specific example, Espresso is not even aware of your secondary process, nor can it maintain any of the synchronization guarantees we all know and love. Today, I'm happy to announce Multiprocess Espresso support. Without changing any of your test code or your app code this will allow you to seamlessly interact with UI across processes, while maintaining all of us Espresso synchronization guarantees. And it will be available in the next version of Android Test Support Library release. So let's have a quick overview of how it actually works. Traditionally, as you know, in our example, we start in one process, where we have an instance of Android JUnit Runner and Espresso, in this case. Now, if you remember from our example, when we click the Add Note button, there will be a new activity and now we have a new process. So the problem now is that we have two processes with two different instances of Android JUnit Runner and Espresso, and they're not aware of each other. So the first thing that we want to do is we want to establish communication between the two Android JUnit Runners. And now that we have this communication, we can use it to establish the communication to Espresso instances. And the way we do that is by having an ability in the Android JUnit Runner to register any testing frameworks, like Espresso, with Android JUnit Runner. And then the runner will then facilitate all the handshaking required in order to establish communication between the two Espresso instances. Now that the two Espresso instances can talk to each other, it can then use it in order to enable cross-process testing and maintain all the synchronization guarantees that we had before. OK. With that, we're reaching the end of our developer workflow and we showed you all the tools that you can use across each step of the way in order to make TDD happen on Android. And with that said, even if you don't follow this flow exactly, hopefully, you know how to use every single tool and how to write good tests in order to bring your app quality to the next level. So if you like to write tests and you want to write and run tests like we do at Google, here are some resources to get you started. I want to thank you and I think we have some time for questions. And if not, we have office hours at 3:30 today. So hopefully, we'll see you there. Thank you. [APPLAUSE] [MUSIC PLAYING]
B1 中級 美國腔 使用Android測試支持庫在Android上進行測試驅動開發(Google I/O'17)。 (Test-Driven Development on Android with the Android Testing Support Library (Google I/O '17)) 41 5 freeman 發佈於 2021 年 01 月 14 日 更多分享 分享 收藏 回報 影片單字