Skip to content
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

Add unit testing page #155

Merged
merged 16 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vitepress/sidebars/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ export default [
{
text: "develop.misc.ideTipsAndTricks",
link: "/develop/ide-tips-and-tricks"
},
{
text: "develop.misc.automatic-testing",
link: "/develop/automatic-testing"
}
]
}
Expand Down
101 changes: 101 additions & 0 deletions develop/automatic-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: Automated Testing
description: A guide to writing automatic tests with Fabric Loader JUnit.
authors:
- kevinthegreat1
---

# Automated Testing {#automated-testing}

This page explains how to write code to automatically test parts of your mod. There are two ways to automatically test your mod: unit tests with Fabric Loader JUnit or game tests with the Gametest framework from Minecraft.

Unit tests should be used to test components of your code, such as methods and helper classes, while game tests spin up an actual Minecraft client and server to run your tests, which makes it suitable for testing features and gameplay.

::: warning
Currently, this guide only covers unit testing.
:::

## Unit Testing {#unit-testing}

Since Minecraft modding relies on runtime byte-code modification tools such as Mixin, simply adding and using JUnit normally would not work. That's why Fabric provides Fabric Loader JUnit, a JUnit plugin that enables unit testing in Minecraft.

### Setting up Fabric Loader JUnit {#setting-up-fabric-loader-junit}

First, we need to add Fabric Loader JUnit to the development environment. Add the following to your dependencies block in your `build.gradle`:

@[code lang=groovy transcludeWith=:::automatic-testing:1](@/reference/build.gradle)

Then, we need to tell Gradle to use Fabric Loader JUnit for testing. You can do so by adding the following code to your `build.gradle`:

@[code lang=groovy transcludeWith=:::automatic-testing:2](@/reference/latest/build.gradle)

#### Split Sources {#split-sources}

::: info
This section is planned to become irrelevant after the release of Loom 1.8. For more information, track [this issue](https://github.com/FabricMC/fabric-loom/issues/1060).
:::

If you are using split sources, you also need to add either the client or server source set to the test source set. Fabric Loader JUnit defaults to client, so we'll add the client source set to our testing environment with the following in `build.gradle`:

@[code lang=groovy transcludeWith=:::automatic-testing:3](@/reference/build.gradle)

### Writing Tests {#writing-tests}

Once you reload Gradle, you're now ready to write tests.

These tests are written just like regular JUnit tests, with a bit of additional setup if you want to access any registry-dependent class, such as `ItemStack`. If you're comfortable with JUnit, you can skip to [Setting Up Registries](#setting-up-registries).

#### Setting Up Your First Test Class {#setting-up-your-first-test-class}

Tests are written in the `src/test/java` directory.

One naming convention is to mirror the package structure of the class you are testing. For example, to test `src/main/java/com/example/docs/codec/BeanType.java`, you'd create a class at `src/test/java/com/example/docs/codec/BeanTypeTest.java`. Notice how we added `Test` to the end of the class name. This also allows you to easily access package-private methods and fields.

Another naming convention is to have a `test` package, such as `src/test/java/com/example/docs/test/codec/BeanTypeTest.java`. This prevents some problems that may arise with using the same package if you use Java modules.

After creating the test class, use <kbd>⌘/CTRL</kbd><kbd>N</kbd> to bring up the Generate menu. Select Test and start typing your method name, usually starting with `test`. Press <kbd>ENTER</kbd> when you're done. For more tips and tricks on using the IDE, see [IDE Tips and Tricks](ide-tips-and-tricks#code-generation).

![Generating a test method](/assets/develop/misc/automatic-testing/unit_testing_01.png)

You can, of course, write the method signature by hand, and any instance method with no parameters and a void return type will be identified as a test method. You should end up with the following:

![A blank test method with test indicators](/assets/develop/misc/automatic-testing/unit_testing_02.png)

Notice the green arrow indicators in the gutter: you can easily run a test by clicking them. Alternately, your tests will run automatically on every build, including CI builds such as GitHub Actions. If you're using GitHub Actions, don't forget to read [Setting Up GitHub Actions](#setting-up-github-actions).

Now, it's time to write your actual test code. You can assert conditions using `org.junit.jupiter.api.Assertions`. Check out the following test:

@[code lang=java transcludeWith=:::automatic-testing:4](@/reference/latest/src/test/java/com/example/docs/codec/BeanTypeTest.java)

For an explanation of what this code actually does, see [Codecs](codecs#registry-dispatch).

#### Setting Up Registries {#setting-up-registries}

Great, the first test worked! But wait, the second test failed? In the logs, we get one of the following errors.

@[code lang=java transcludeWith=:::automatic-testing:5](@/reference/latest/src/test/java/com/example/docs/codec/BeanTypeTest.java)

This is because we're trying to access the registry or a class that depends on the registry (or, in rare cases, depends on other Minecraft classes such as `SharedConstants`), but Minecraft has not been initialized. We just need to initialize it a little bit to have registries working. Simply add the following code to the beginning of your `beforeAll` method.

@[code lang=java transcludeWith=:::automatic-testing:7](@/reference/latest/src/test/java/com/example/docs/codec/BeanTypeTest.java)

### Setting Up GitHub Actions {#setting-up-github-actions}

::: info
This section assumes that you are using the standard GitHub Action workflow included with the example mod and with the mod template.
:::

Your tests will now run on every build, including those by CI providers such as GitHub Actions. But what if a build fails? We need to upload the logs as an artifact so we can view the test reports.

Add this to your `.github/workflows/build.yml` file, below the `./gradlew build` step.

```yaml
- name: Store reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: reports
path: |
**/build/reports/
**/build/test-results/
```
2 changes: 1 addition & 1 deletion develop/codecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ Codec<BeanType<?>> beanTypeCodec = BeanType.REGISTRY.getCodec();
// And based on that, here's our registry dispatch codec for beans!
// The first argument is the field name for the bean type.
// When left out, it will default to "type".
Codec<Bean> beanCodec = beanTypeCodec.dispatch("type", Bean::getType, BeanType::getCodec);
Codec<Bean> beanCodec = beanTypeCodec.dispatch("type", Bean::getType, BeanType::codec);
```

Our new codec will serialize beans to json like this, grabbing only fields that are relevant to their specific type:
Expand Down
14 changes: 14 additions & 0 deletions develop/ide-tips-and-tricks.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ For example, when using Lambdas, you can write them quickly using this method.

![Lambda with many parameters](/assets/develop/misc/using-the-ide/util_01.png)

### Code Generation {#code-generation}

The Generate menu can be quickly accessed with <kbd>⌘/CTRL</kbd><kbd>N</kbd>.
In a Java file, you will be able to generate constructors, getters, setters, and override or implement methods, and much more.
You can also generate accessors and invokers if you have the [Minecraft Development plugin](./getting-started/setting-up-a-development-environment#minecraft-development) installed.

In addition, you can quickly override methods with <kbd>⌘/CTRL</kbd><kbd>O</kbd> and implement methods with <kbd>⌘/CTRL</kbd><kbd>I</kbd>.

![Code generation menu in a Java file](/assets/develop/misc/using-the-ide/generate_01.png)

In a Java test file, you will be given options to generate related testing methods, as follows:

![Code generation menu in a Java test file](/assets/develop/misc/using-the-ide/generate_02.png)

### Displaying Parameters{#displaying-parameters}

Displaying parameters should be activated by default. You will automatically get the types and names of the parameters while writing your code.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 14 additions & 1 deletion reference/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,21 @@ subprojects {
}
}

// :::automatic-testing:3
sourceSets {
test {
compileClasspath += client.compileClasspath
runtimeClasspath += client.runtimeClasspath
}
}
// :::automatic-testing:3

dependencies {
modImplementation "net.fabricmc:fabric-loader:0.15.11"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"

// :::automatic-testing:1
testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
kevinthegreat1 marked this conversation as resolved.
Show resolved Hide resolved
// :::automatic-testing:1
}

fabricApi {
Expand Down
4 changes: 3 additions & 1 deletion reference/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
org.gradle.parallel=true

loader_version=0.15.11
6 changes: 6 additions & 0 deletions reference/latest/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ dependencies {
mappings "net.fabricmc:yarn:${yarnVersion}:v2"
modImplementation "net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}"
}

// :::automatic-testing:2
test {
useJUnitPlatform()
}
// :::automatic-testing:2
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package com.example.docs.codec;

import com.mojang.serialization.Codec;

// :::
// The abstract type we want to create a codec for
public interface Bean {
// Now we can create a codec for bean types based on the previously created registry.
Codec<Bean> BEAN_CODEC = BeanType.REGISTRY.getCodec()
// And based on that, here's our registry dispatch codec for beans!
// The first argument is the field name for the bean type.
// When left out, it will default to "type".
.dispatch("type", Bean::getType, BeanType::codec);

BeanType<?> getType();
}
// :::
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.example.docs.codec;

import com.mojang.serialization.Codec;
import com.mojang.serialization.Lifecycle;
import com.mojang.serialization.MapCodec;

import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
Expand All @@ -11,7 +11,7 @@
// :::
// A record to keep information relating to a specific
// subclass of Bean, in this case only holding a Codec.
public record BeanType<T extends Bean>(Codec<T> codec) {
public record BeanType<T extends Bean>(MapCodec<T> codec) {
// Create a registry to map identifiers to bean types
public static final Registry<BeanType<?>> REGISTRY = new SimpleRegistry<>(
RegistryKey.ofRegistry(Identifier.of("example", "bean_types")), Lifecycle.stable());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public class BeanTypes {
public static final BeanType<StringyBean> STRINGY_BEAN = register("stringy_bean", new BeanType<>(StringyBean.CODEC));
public static final BeanType<CountingBean> COUNTING_BEAN = register("counting_bean", new BeanType<>(CountingBean.CODEC));

//:::
public static void register() { }

//:::
public static <T extends Bean> BeanType<T> register(String id, BeanType<T> beanType) {
return Registry.register(BeanType.REGISTRY, Identifier.of("example", id), beanType);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.example.docs.codec;

import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;

// :::
// Another implementation
public class CountingBean implements Bean {
public static final Codec<CountingBean> CODEC = RecordCodecBuilder.create(instance -> instance.group(
public static final MapCodec<CountingBean> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.INT.fieldOf("counting_number").forGetter(CountingBean::getCountingNumber)
).apply(instance, CountingBean::new));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.example.docs.codec;

import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;

// :::
// An implementing class of Bean, with its own codec.
public class StringyBean implements Bean {
public static final Codec<StringyBean> CODEC = RecordCodecBuilder.create(instance -> instance.group(
public static final MapCodec<StringyBean> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.STRING.fieldOf("stringy_string").forGetter(StringyBean::getStringyString)
).apply(instance, StringyBean::new));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.example.docs.codec;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.mojang.serialization.JsonOps;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import net.minecraft.Bootstrap;
import net.minecraft.SharedConstants;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;

// :::automatic-testing:4
public class BeanTypeTest {
private static final Gson GSON = new GsonBuilder().create();

@BeforeAll
static void beforeAll() {
// :::automatic-testing:4
// :::automatic-testing:7
SharedConstants.createGameVersion();
Bootstrap.initialize();
// :::automatic-testing:7
// :::automatic-testing:4
BeanTypes.register();
}

@Test
void testBeanCodec() {
StringyBean expectedBean = new StringyBean("This bean is stringy!");
Bean actualBean = Bean.BEAN_CODEC.parse(JsonOps.INSTANCE, GSON.fromJson("{\"type\":\"example:stringy_bean\",\"stringy_string\":\"This bean is stringy!\"}", JsonObject.class)).getOrThrow();

Assertions.assertInstanceOf(StringyBean.class, actualBean);
Assertions.assertEquals(expectedBean.getType(), actualBean.getType());
Assertions.assertEquals(expectedBean.getStringyString(), ((StringyBean) actualBean).getStringyString());
}

@Test
void testDiamondItemStack() {
// I know this isn't related to beans, but I need an example :)
ItemStack diamondStack = new ItemStack(Items.DIAMOND, 65);

Assertions.assertTrue(diamondStack.isOf(Items.DIAMOND));
Assertions.assertEquals(65, diamondStack.getCount());
}
}
// :::automatic-testing:4

/*
// :::automatic-testing:5
java.lang.ExceptionInInitializerError
at net.minecraft.item.ItemStack.<clinit>(ItemStack.java:94)
at com.example.docs.codec.BeanTypeTest.testBeanCodec(BeanTypeTest.java:20)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.IllegalArgumentException: Not bootstrapped (called from registry ResourceKey[minecraft:root / minecraft:game_event])
at net.minecraft.Bootstrap.createNotBootstrappedException(Bootstrap.java:118)
at net.minecraft.Bootstrap.ensureBootstrapped(Bootstrap.java:111)
at net.minecraft.registry.Registries.create(Registries.java:238)
at net.minecraft.registry.Registries.create(Registries.java:229)
at net.minecraft.registry.Registries.<clinit>(Registries.java:139)
... 5 more

Not bootstrapped (called from registry ResourceKey[minecraft:root / minecraft:game_event])
java.lang.IllegalArgumentException: Not bootstrapped (called from registry ResourceKey[minecraft:root / minecraft:game_event])
at net.minecraft.Bootstrap.createNotBootstrappedException(Bootstrap.java:118)
at net.minecraft.Bootstrap.ensureBootstrapped(Bootstrap.java:111)
at net.minecraft.registry.Registries.create(Registries.java:238)
at net.minecraft.registry.Registries.create(Registries.java:229)
at net.minecraft.registry.Registries.<clinit>(Registries.java:139)
at net.minecraft.item.ItemStack.<clinit>(ItemStack.java:94)
at com.example.docs.codec.BeanTypeTest.testBeanCodec(BeanTypeTest.java:20)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
// :::automatic-testing:5
*/
1 change: 1 addition & 0 deletions sidebar_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"develop.misc.events": "Events",
"develop.misc.text-and-translations": "Text and Translations",
"develop.misc.ideTipsAndTricks": "IDE Tips and Tricks",
"develop.misc.automatic-testing": "Automated Testing",
"develop.sounds": "Sounds",
"develop.sounds.using-sounds": "Playing Sounds",
"develop.sounds.custom": "Creating Custom Sounds"
Expand Down