In this overview, I'll be covering integration testing concepts, using repositories and mocking in Flutter. Part 2 of this article will cover a detailed code walkthrough so stay tuned here and at https://youtube.com/c/seenickcode!
Integration testing ensures that your app can run properly on either a real device or a simulator and that the main components of your app seamless work together without any issue.
In addition to integration testing, we also have something called unit testing, where each individual atomic "unit" is tested. In the Flutter world, unit testing is also known as widget testing, where we ensure individual widgets work the way we want them to work.
This tutorial will cover the former, integration testing.
What we fundamentally prove with an integration test, at the very least, with even just a few lines of code, is if our app can startup and load properly. The benefit here is that if something goes wrong, say that there's a bug that is only exposed when we distribute the app and it's started by users, we catch it early on in the development lifecycle.
Another thing that we prove out in integration testing is, typically, can our individual screens load, can a user transition from screen to screen and are basic interactions, such as tapping buttons, entering text in a form, etc triggering the desired output?
For example, a typical integration test would start our app, load up the first screen for the user (again, on a real device or simulator), we interact with a few critical pieces of functionality and that the app behaves as we expect, especially without any hanging, crashing or without any undesired interactions.
So why does this title mention "repositories" and "mocking"? Well, these tie in typically with integration testing.
You see, it is not typically best practice to rely on external services when we run an integration test. An external service is typically a web service, for example. This is because that specific service has its own test. In other words, we know it "works". Second, calling out to external services slow down the integration testing process. Our tests can break if that service is down for maintenance or unavaialble. Our integration tests shouldn't rely on external services to simply run. We want our external service which our tests may rely on to be predictable.
That's why we typically will mock the external service with a bit of code. This gives us fine grained control of its behavior.
Now, how can we mock external services? Well, if we utilize a specific pattern called the "repository pattern", we can, depending on how our app is run (either via an integration test or via normal execution), our code can choose to mock or not mock.
The repository pattern's role is to simply abstract away the "how" of data access. Say we have a bit of code that calls out to a web service, we can wrap that logic in a method that allows the developer to swap out this logic "under the hood" if he or she chooses to do so.
We create a "repository" which can be a standalone class, containing methods that allow us to define a layer that other parts of our code can use, without being concerned about how we access the data.
For an example, we can create a "user repository", again, just a class, that has a single method, "get user". The "get user" implementation will contain all the nitty gritty business logic on how we call out to a web service, using HTTP, JSON, deserialization, etc.
Now, take our frontend, UI code in a random Flutter widget. If this widget needs to get a user, for example, the code for this is very simple, we just interact with an instance of a "user repository" and call our method. No messy code mucking up our widget.
What's cool about this pattern is that, it's like really anything else in software development, where a codebase offers abstractions upon abstractions upon abstractions. Kind of like an onion. We do this to make our code modular, easy to use (especially if we're working with other developers) and as easy as possible to refactor and change.
What's even more cool about this pattern is that, as it pertains to integration testing, we can "mock" our repositories by creating different variations of them, depending on what we need.
An example of a "mock" "user repository", from our example above, can have a "get user" method that simply returns a hardcoded user, instead of calling out to an external service.
Overall, mocking is an important concept in software testing, be it in frontend development, backend development or really anything you can think of.
Additionally, repositories allow us to greatly simplify our code. When it comes to sometimes, messy, boilerplate code to access data sources, such as a web service or a database, repositories provide a nice layer of abstraction, especially when working with a development team, large codebase or for our case mentioned above, integration tests.
For a continuation of this overview with a real coding walkthrough, check out my YouTube channel: https://youtube.com/c/seenickcode where I post a new full stack intermediate Flutter tutorial every other week.