Writing Java tests for Fabric
See original GitHub issueWhile the gametest framework is a fantastic utility for testing Minecraft mods, there’s often times when it’s desirable to write unit tests more isolated integration tests[^1] instead. However, as mixins aren’t applied inside a test environment, there’s only so much you can do before hitting LinkageError
or other fun runtime issues!
Several mods have solved this by injecting Fabric loader as part of the test initialisation process (for example multiconnect and AE2). However, given this is quite an involved process (and is quite heavily tied to Fabric Loader internals), it feels like it would be good to have an official solution instead.
This obviously isn’t something Loom can solve in isolation (it would, I assume require some changes to Fabric Loader and/or DLI), but given it’s dev-tooling related, this seemed the best place.
This is the current solution I’m using. It hooks into the test lifecycle early on using JUnit’s automatic extension registration[^2], then installs a Java agent, acquiring an Instrumentation
interface, which is then used for bytecode rewriting. To be clear, this isn’t good code, but hopefully shows that the underlying principle is viable!
To build something actually usable, I’d probably propose the following changes:
-
Add a new launcher class to Fabric Loader which loads mods and installs a
ClassFileTransformer
into a providedInstrumentation
, but does not actually launch the game. -
Define a JUnit extension somewhere (in DLI would sorta make sense, but could be a new project), which installs a Java agent and sets up Fabric Loader.
The pain point here is that, unlike DLI, the extension shouldn’t rely on any system properties or arguments (as those won’t be present when running tests from an IDE). Short of hard-coding
.gradle/loom-cache/launch.cfg
into the extension, I’m not sure of a good way to detect the additional launch properties needed. -
Add some method to Loom (
loom.enableTestSupport()
) which adds the test extension to the test runtime classpath.
I’m happy to take a look at implementing this, just wanted to check whether this was desirable and/or sensible.
[^1]: Player objects me calling these unit tests. More precisely, I want to write smaller-scale tests which only depend on a small subset of Minecraft’s functionality, and so don’t require spinning up a server.
[^2]: This does require setting a system property, which is annoying. There’s probably other ways to nastily load early on, such as defining a TestEngine
,
Issue Analytics
- State:
- Created a year ago
- Reactions:5
- Comments:11 (8 by maintainers)
Top GitHub Comments
Oh, that’s fairly easy to do already - just add
dependencies { testImplementation(sourceSets.client.output) }
to your Gradle script. But possibly should be adding that to Loom too.Yep, that’s pretty much what I did in CC:T (I use
ClassInjector.UsingReflection
rather thanUsingInstrumentation
). It’s definitely workable, just feels sufficiently ugly I wouldn’t want to build an “official” solution on top of it :p.