-
Notifications
You must be signed in to change notification settings - Fork 117
Use dependency injection for complete end-to-end testing of APRS service #319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This makes creating UDP Datagram sockets as an injectable dependency and provides a mock version of it for an instrumented test to capture outgoing packets instead of sending them over the wire for the test.
The Kotlin code was causing build errors on every file in the compileDebugScala task. It appears like it's trying to compile it as Scala or something. Avoiding Kotlin until this can be resolved. Fixed up branch to rebase it off of master Reverted Kotlin plugin changes As Kotlin can't be used currently, leave out the changes to support it.
Most preferences are actually saved as strings. Also, functions/methods not called in the main app get optimized out and are not available to be called in the test framework. Make sure that all functions needed by the test are also called from within the app.
The service locator is now stored per application context instead of staticly.
The tests are now passing reliably now that all the correct preferences are being set for the test.
This is the first test to acquire a location from the Android system and test that it will produce an appropriate packet and upload it.
This was triggering an illegal argument error when run on real hardware.
|
I've just pushed one more commit with the first test of the PeriodicGPS LocationSource. This uses the Android API for creating a Mock Location Provider to inject actual Location objects into the system location service for the app to receive and process. This being a feature of Android itself did not require my additions to inject a new dependency. Instead, I just had to add the permission for MOCK_LOCATION to the Android manifest. I put it in the debug folder so it's only merged in during debug build and kept out of release builds. I do use my injected DatagramRecorderSocket() to capture the data sent to APRS-IS for upload. So, with the mock location support and the injected DatagramSocket, this is a complete, end-to-end test of the service receiving GPS location and uploading it to the cloud. I tweaked the location slightly from the manual location test to make sure I wasn't getting a false positive from the manual test. This test can be run on a real device as well as the Android emulator, you just need to allow mock locations. The following commands should be able to run the full test suite: Or, you can be selective and just run this specific test with this: The tests themselves are both a bit rough and could use some improvement, but this is just to get a proof-of-concept out the door to see how well this might work. Also, beyond using a mock location provider, you can use the |
This is just an experiment and proof of concept to test the feasibility of using dependency injection to test more aspects of APRSdroid. I purposely tried to keep it as minimal as possible while allowing a full end-to-end test of a LocationSource to generate a position to sending out the packet up to APRS-IS (without actually sending it through the Internet). I'm curious what your thoughts are on this approach and taking this further. This PR is based on master to keep it easy to follow.
As you've probably guess by now, I like testing and I would like to see if a majority of the code can be tested to validate it's current behavior before doing any major changes or enhancements especially if one of those is rewriting it in a different language. I also like to create a test validating the expected behavior of any bug reported by a user before fixing it so I can verify that the bug is both properly detected and later fixed, and that any regression in the future will be caught. The latitude/longitude bug on the manual location selection screen was a good example to try that on.
First, I'll give a quick explanation of dependency injection just in case anyone reading this hasn't had the luxury of working with that yet. The basic idea is to replace any strict dependency with a more loosely-coupled interface so that the dependency can be swapped out. Dependencies here generally refers to other classes that are used to support some operation. As a specific example of that, the UdpUploader backend has a strict dependency on the Java DatagramSocket class. It will instantiate a new one each time the service is started up. The UdpUploader can't be tested by itself without triggering real network traffic to a service. By applying the principles of dependency inversion, we can change it to use a factory that is provided externally, typically through it's constructor somehow, and it can call that factory to generate the DatagramSockets it needs. We can then inject a carefully constructed mock during a test that can do something else beside send UDP to the Internet.
In the androidTest folder, you can see a mock called DatagramRecorderSocket. It extends the real Java DatagramSocket, but stubs out several methods to do nothing and saves all packets to be sent internally in a log. This is used by the test in ManualLocationUploadTest to verify that the APRS service is working and uploads exactly one packet to APRS-IS that is parsed to verify it's formatted correctly and matched the expected coordinates and login information. This verifies all the code used by the service right up to the point that it passes into the Java networking library and leaves APRSdroid.
I had to modify 4 existing source files to support this, but I tried it keep it small. Also, I wrote it to be entirely based on manual dependency injection so there's no hidden magic involved. An instance of the service locator that has the factory provider for DatagramSocket is kept in the Application context. All components that need those dependencies can get a handle to it from there. When a test is run, it will first grab the application context and swap out the service locator for one that provides the appropriate mocks needed for the test. This is used at the end of the test to examine what datagrams were transmitted after configuring the application preferences and starting the service for a one-shot upload. The code itself could use a little clean-up, but it is working as a first demo.
If this is taken further, next could be creating a provider for the LOCATION_SERVICE used by the SmartBeaconing LocationSource, then create a mock that will call back from the test to the LocationListener provided to it and inject a series of pre-recorded positions. It can send a series of coordinates mostly on top of each other or in a straight line, then add in a 90 degree bend and the test can verify the UdpUploader datagram logs to see if the 3 key points of this right angle have been uploaded, but not most of the remaining noise that smart beaconing filters out.
Or, another thing I'd like to see would be to mock out the BluetoothTnc and have it receive a series of lines of text such as the NMEA log from the Kenwood D72 and verify how APRSdroid handles it. And for special cases like the Yaesu FTM-400XDR that split packets with newlines unexpectedly, saving those logs from a real device and injecting them from a test to make sure they are still parsed correctly.
Also, if this is taken further, since dependency injection can become a bit noisy and harder to follow, there are a number of common support libraries including Mockito, Dagger, and Hilt that can simply the code quite a bit and keep it uniform. I'm working on integrating Hilt with own apps right now. However, even doing it manually, but carefully now might be a nice approach for improving testability.