Refactoring so GWT unit tests run less slowly #4460
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
The GWT unit tests (
ant unittest
) was taking over a minute to run each time, making it painful to use for... unit testing.The problem (besides the inherent slowness of GWTTestCase and its need to run a hidden browser) are classes internally invoking dependency injection, such as
RStudioGinjector.INSTANCE.injectMembers(this)
orRStudioGinjector.INSTANCE.getCommands()...
and so on.By doing this, they were dragging in pretty much the entire codebase for each test in order to perform the injection of the real object set. This is (very) slow to startup, and also goes against the very notion of unit testing (and defeats a big reason for using the dependency injection framework) as these tests are essentially doing integration testing of many classes at once instead of testing a single class in a well-controlled manner.
The goal, then is to minimize calls to the injector, and instead rely on injection via constructors, so tests can supply mock/fake alternatives.
VirtualConsole
The
VirtualConsole
class, when constructed, was injecting UIPrefs into itself so it could react to a couple of preferences. I fixed this by defining an interface,VirtualConsole.Preferences
, which defines just the accessors needed by VirtualConsole. That interface is injected via assisted injection to the VirtualConsole constructor. At product runtime, Gin injects an instance of a newVirtualConsolePreferences
object implementing that interface (which itself gets the actual UIPrefs object injected); in the tests, where dependency injection is not used, we define and pass in our own fake implementation of the interface.Assisted injection allows injection where the constructor needs some arguments provided by the caller, along with those being injected. This requires wiring up and using a Gin factory interface; see
VirtualConsoleFactory
.Several places in the production code that create VirtualConsole's went from this pattern:
to
ConsoleOutputWriter
This class internally allocates a VirtualConsole object (originally via new); now it is passed the VirtualConsoleFactory interface and uses that. The sole caller of ConsoleOutputWriter is
ShellWidget
which isn't currently under test, so kicked the can down the road and let it do the dependency injection of the factory viagetVirtualConsoleFactory()
. This lets us test ConsoleOutputWriter itself by supplying it our own fake VirtualConsoleFactory.JobsList / JobItem
These classes are UI widgets that also contain quite a bit of logic; ideally I'd separate them into a cleaner model/view/presenter model and focus on testing the presenter, but I had written tests directly against them and doubt I'll ever get to that exercise.
JobItem
objects are allocated by JobsList, and then using RStudioGinjector.INSTANCE to get at the EventBus and at a "stop" command icon. Thus bogging down the tests.Again I implemented the assisted injection pattern on them, and pass in a new interface
FireEvents
(extracted from EventBus); in production code this is injected by Gin as EventBus; in test I can pass in my own fake implementation. For getting at the icon, I just used the one that had already been added but not referenced and avoided the need to abstractCommands
.With all of this the unit tests are running in about 15 - 20 seconds. Suspect improving this would require moving some tests to be pure Java / JUnit, and additional abstraction to reduce amount of UI code being tested.
Pro will require additional work (it has a couple of extra Pro-only tests); those will still be slow, but this change can be merged into Pro as-is.