Test a plugin
This page shows where the tests of a plugin go, how to test its registration and a whole start, and how to test it against a real server in Docker.
1. Run the tests that exist
Section titled “1. Run the tests that exist”dotnet test tests/Tilde.TestsThe unit tests need nothing but the .NET 10 SDK. The CI builds with warnings as errors, so build the same way before you open a pull request:
dotnet build Tilde.slnx -c Release -warnaserrorWhere the tests go
Section titled “Where the tests go”- Unit tests go in
tests/Tilde.Tests/Plugin/<Mail or Chat>/<Name>/<Subject>Tests.cs, with the namespaceTilde.Tests.Plugin.Mail.Mboxfor the mbox plugin. One test class per subject. tests/Tilde.Tests/Tilde.Tests.csprojreferences the project of the plugin, and the plugin gives its internals to the tests with<InternalsVisibleTo Include="Tilde.Tests" />.- Shared fakes and helpers go in
tests/Tilde.Tests/Support/, such asSupport/Mbox/TempMbox.cs, which writes mbox files in a temporary folder. - A test is named
Method_Scenario_ExpectedResultand has the comments// Arrange,// Actand// Assert.
These rules are in CODE_CONVENTION.md, section 10.
Test the registration
Section titled “Test the registration”Give RegisterAsync a new container and a ConfigService on a file that does not exist, then ask the container for what the plugin added. From MboxPluginTests.cs:
[Fact]public async Task RegisterAsync_AddsTheMboxFactoryToTheMailStoreFactories(){ // Arrange using var container = new Container(); container.RegisterInstance<IEventHubService>(new EventHubService()); var plugin = new MboxPlugin();
// Act await plugin.RegisterAsync( container, new ConfigService("unused.toml", new Moongate.Core.Expansion.ValueExpander()), default );
// Assert var factories = container.Resolve<IEnumerable<IConnectionFactory<IMailStore>>>(); Assert.Equal("mbox", Assert.Single(factories).Protocol);}A plugin with a section is tested the same way, with a real file: write the TOML in a temporary folder and pass its path to ConfigService.
Test a whole start
Section titled “Test a whole start”TildeBootstrap runs the plugin as tcli and tt do: it reads a configuration file, registers the plugins and starts the services. From MboxBootstrapTests.cs, where WriteConfigAsync writes a config.toml with one mbox account, archive:
// Arrange_mbox.AddFolder( "INBOX", MboxSamples.Message("Read one", status: "RO"), MboxSamples.Message("Unread one"));var config = await WriteConfigAsync(_mbox.Root);await using var bootstrap = new TildeBootstrap(config);bootstrap.AddPlugin<MboxPlugin>();await bootstrap.ConfigureAsync();
// Actawait bootstrap.StartAsync();
// Assertvar store = bootstrap.Container.Resolve<IAccountService>().GetMailStore("archive");Assert.Equal(ConnectionStateType.Connected, store.State);var item = Assert.Single(bootstrap.Container.Resolve<IWhoNeedsMeService>().Items);Assert.Equal("Unread one", item.Title);Assert.Equal(AttentionKindType.UnreadMail, item.Kind);A plugin that needs another one adds it too: bootstrap.AddPlugin(new ScriptingPlugin(scripts)) before its own, with the folder of the test scripts. Without it, ConfigureAsync throws The plugin 'example.hello' needs 'tilde.scripting', which is not registered. The plugin of Commands, Lua modules and events was tested this way, with an init.lua in that folder and the commands run through ICommandService.RunLineAsync.
Test against a real server
Section titled “Test against a real server”A test that needs a real server goes in tests/Tilde.Tests.Integration, with the same folders (Plugin/Mail/Imap, Plugin/Chat/Irc). Testcontainers starts the server in Docker, once for a collection of tests, and a test is skipped when Docker or the image is not there. The servers in use are GreenMail (IMAP and SMTP), ejabberd (XMPP) and Ergo (IRC), started by the fixtures in tests/Tilde.Tests.Integration/Support. From ImapConnectionTests.cs:
[Collection(GreenMailCollection.Name)]public sealed class ImapConnectionTests{ private readonly GreenMailFixture _greenMail;
public ImapConnectionTests(GreenMailFixture greenMail) { _greenMail = greenMail; }
[SkippableFact] public async Task ConnectAsync_WithValidCredentials_ConnectsAndPublishesTheStates() { // Arrange Skip.If(_greenMail.UnavailableReason is not null, _greenMail.UnavailableReason); var account = await _greenMail.CreateAccountAsync(); var hub = new EventHubService(); await using var store = new ImapMailStore("work", account.Settings(), hub);
// Act await store.ConnectAsync();
// Assert Assert.Equal(ConnectionStateType.Connected, store.State); }}That is the test with its check of the events left out. The fixture catches the error of a missing Docker in InitializeAsync and keeps it in UnavailableReason. The Support folder also has small fake servers on the loopback address (FakeSmtpServer, FakeIrcServer, FakeXmppServer) for the cases a real server cannot show, such as a connection that drops while a message goes out.
Run one group at a time, as the CI does:
dotnet test tests/Tilde.Tests.Integration --filter "FullyQualifiedName~Tilde.Tests.Integration.Plugin.Mail"The other groups are Plugin.Chat.Xmpp, Plugin.Chat.Irc and Plugin.Pgp. The jobs are in .forgejo/workflows/ci.yml; a new group of integration tests gets a job there.
A plugin with a package
Section titled “A plugin with a package”The CI checks that THIRD-PARTY-NOTICES.md lists every package. A plugin that adds a NuGet package runs scripts/generate-third-party-notices.sh, which needs the nuget-license tool as in the CI job, and commits the two notice files it writes. More on the CI is on Releases and CI, and on the build on Build and test.