diff --git a/.vitepress/sidebars/develop.ts b/.vitepress/sidebars/develop.ts
index 1e2b097e4..fe327e419 100644
--- a/.vitepress/sidebars/develop.ts
+++ b/.vitepress/sidebars/develop.ts
@@ -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"
}
]
}
diff --git a/develop/automatic-testing.md b/develop/automatic-testing.md
new file mode 100644
index 000000000..86959d9b9
--- /dev/null
+++ b/develop/automatic-testing.md
@@ -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 ⌘/CTRLN to bring up the Generate menu. Select Test and start typing your method name, usually starting with `test`. Press ENTER 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/
+```
diff --git a/develop/codecs.md b/develop/codecs.md
index af4a55d15..418012cef 100644
--- a/develop/codecs.md
+++ b/develop/codecs.md
@@ -390,7 +390,7 @@ Codec> 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 beanCodec = beanTypeCodec.dispatch("type", Bean::getType, BeanType::getCodec);
+Codec 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:
diff --git a/develop/ide-tips-and-tricks.md b/develop/ide-tips-and-tricks.md
index 0cd5bbc72..338cde619 100644
--- a/develop/ide-tips-and-tricks.md
+++ b/develop/ide-tips-and-tricks.md
@@ -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 ⌘/CTRLN.
+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 ⌘/CTRLO and implement methods with ⌘/CTRLI.
+
+![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.
diff --git a/public/assets/develop/misc/automatic-testing/unit_testing_01.png b/public/assets/develop/misc/automatic-testing/unit_testing_01.png
new file mode 100644
index 000000000..1795e3c53
Binary files /dev/null and b/public/assets/develop/misc/automatic-testing/unit_testing_01.png differ
diff --git a/public/assets/develop/misc/automatic-testing/unit_testing_02.png b/public/assets/develop/misc/automatic-testing/unit_testing_02.png
new file mode 100644
index 000000000..246ed7221
Binary files /dev/null and b/public/assets/develop/misc/automatic-testing/unit_testing_02.png differ
diff --git a/public/assets/develop/misc/using-the-ide/generate_01.png b/public/assets/develop/misc/using-the-ide/generate_01.png
new file mode 100644
index 000000000..6493d27b7
Binary files /dev/null and b/public/assets/develop/misc/using-the-ide/generate_01.png differ
diff --git a/public/assets/develop/misc/using-the-ide/generate_02.png b/public/assets/develop/misc/using-the-ide/generate_02.png
new file mode 100644
index 000000000..abc4e1ee9
Binary files /dev/null and b/public/assets/develop/misc/using-the-ide/generate_02.png differ
diff --git a/reference/build.gradle b/reference/build.gradle
index 09806bac0..ba42925ad 100644
--- a/reference/build.gradle
+++ b/reference/build.gradle
@@ -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}"
+ // :::automatic-testing:1
}
fabricApi {
diff --git a/reference/gradle.properties b/reference/gradle.properties
index dd68d7614..34c7e8138 100644
--- a/reference/gradle.properties
+++ b/reference/gradle.properties
@@ -1,2 +1,4 @@
org.gradle.jvmargs=-Xmx1G
-org.gradle.parallel=true
\ No newline at end of file
+org.gradle.parallel=true
+
+loader_version=0.15.11
\ No newline at end of file
diff --git a/reference/latest/build.gradle b/reference/latest/build.gradle
index b636d6a16..3721255ac 100644
--- a/reference/latest/build.gradle
+++ b/reference/latest/build.gradle
@@ -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
diff --git a/reference/latest/src/main/java/com/example/docs/codec/Bean.java b/reference/latest/src/main/java/com/example/docs/codec/Bean.java
index f02a5f35d..8517df2db 100644
--- a/reference/latest/src/main/java/com/example/docs/codec/Bean.java
+++ b/reference/latest/src/main/java/com/example/docs/codec/Bean.java
@@ -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_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();
}
// :::
diff --git a/reference/latest/src/main/java/com/example/docs/codec/BeanType.java b/reference/latest/src/main/java/com/example/docs/codec/BeanType.java
index 52effe3c1..da1e8ce25 100644
--- a/reference/latest/src/main/java/com/example/docs/codec/BeanType.java
+++ b/reference/latest/src/main/java/com/example/docs/codec/BeanType.java
@@ -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;
@@ -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(Codec codec) {
+public record BeanType(MapCodec codec) {
// Create a registry to map identifiers to bean types
public static final Registry> REGISTRY = new SimpleRegistry<>(
RegistryKey.ofRegistry(Identifier.of("example", "bean_types")), Lifecycle.stable());
diff --git a/reference/latest/src/main/java/com/example/docs/codec/BeanTypes.java b/reference/latest/src/main/java/com/example/docs/codec/BeanTypes.java
index b1cbed28a..62af651cb 100644
--- a/reference/latest/src/main/java/com/example/docs/codec/BeanTypes.java
+++ b/reference/latest/src/main/java/com/example/docs/codec/BeanTypes.java
@@ -11,6 +11,10 @@ public class BeanTypes {
public static final BeanType STRINGY_BEAN = register("stringy_bean", new BeanType<>(StringyBean.CODEC));
public static final BeanType COUNTING_BEAN = register("counting_bean", new BeanType<>(CountingBean.CODEC));
+ //:::
+ public static void register() { }
+
+ //:::
public static BeanType register(String id, BeanType beanType) {
return Registry.register(BeanType.REGISTRY, Identifier.of("example", id), beanType);
}
diff --git a/reference/latest/src/main/java/com/example/docs/codec/CountingBean.java b/reference/latest/src/main/java/com/example/docs/codec/CountingBean.java
index e6b33f437..785b9e2ad 100644
--- a/reference/latest/src/main/java/com/example/docs/codec/CountingBean.java
+++ b/reference/latest/src/main/java/com/example/docs/codec/CountingBean.java
@@ -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 CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.INT.fieldOf("counting_number").forGetter(CountingBean::getCountingNumber)
).apply(instance, CountingBean::new));
diff --git a/reference/latest/src/main/java/com/example/docs/codec/StringyBean.java b/reference/latest/src/main/java/com/example/docs/codec/StringyBean.java
index be1f64e71..e96ce5a9b 100644
--- a/reference/latest/src/main/java/com/example/docs/codec/StringyBean.java
+++ b/reference/latest/src/main/java/com/example/docs/codec/StringyBean.java
@@ -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 CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.STRING.fieldOf("stringy_string").forGetter(StringyBean::getStringyString)
).apply(instance, StringyBean::new));
diff --git a/reference/latest/src/test/java/com/example/docs/codec/BeanTypeTest.java b/reference/latest/src/test/java/com/example/docs/codec/BeanTypeTest.java
new file mode 100644
index 000000000..5a7d46170
--- /dev/null
+++ b/reference/latest/src/test/java/com/example/docs/codec/BeanTypeTest.java
@@ -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.(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.(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.(Registries.java:139)
+ at net.minecraft.item.ItemStack.(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
+ */
diff --git a/sidebar_translations.json b/sidebar_translations.json
index de3079a25..180cd7850 100644
--- a/sidebar_translations.json
+++ b/sidebar_translations.json
@@ -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"