OpenBI Docs Buy

Testing

A plug-in’s logic and agent tools need only IStudioHost. Give them a small host of your own in a test project, and call the tools as an agent would: no studio, and no Windows, needed.

The pattern

  • Keep the logic and the tools in the plain net10.0 library, apart from any WPF view.
  • Write a fake IStudioHost that answers what your tests need: the answers to its OAX queries, the project’s state; and keep what the plug-in writes to the Activity panel, to check it.
  • Make your tool classes with it, call their methods, and check what they return: that text is what the agent reads.

Customer Service is tested this way: a FakeStudioHost and a mailbox in memory, with no studio and no mail server (see Its tests).

A fake host

For Model Summary, from Your first plug-in. The members with a body of their own (the data plug-in members of contract 1.1, RequestAgentAccessAsync of 1.2) need nothing.

using OpenBI.Studio.Core.Engine;
using OpenBI.Studio.Core.Privacy;
using OpenBI.Studio.Sdk;

namespace ModelSummary.Tests;

/// <summary>
/// The studio, as far as Model Summary's tests need it: the UI thread is the test's own, the model answers what Dax says,
/// and the Activity panel's lines are kept in Logged. The members of IStudioHost with a body of their own (contract 1.1's
/// data plug-in members) need nothing here.
/// </summary>
public sealed class FakeHost : IStudioHost
{
    public Func<string, QueryResult>? Dax { get; set; }
    public IReadOnlyList<TableInfo> Tables { get; set; } = [];
    public List<string> Logged { get; } = [];

    public string StudioVersion => "0.3.74";
    public Task InvokeAsync(Action action) { action(); return Task.CompletedTask; }
    public Task<T> InvokeAsync<T>(Func<T> func) => Task.FromResult(func());
    public void Post(Action action) => action();
    public object MainWindow { get; } = new();

    public bool HasProject => Dax is not null;
    public string? ProjectName => HasProject ? "Test" : null;
    public string? ProjectFolder => null;
    public event EventHandler? ProjectChanged { add { } remove { } }
    public Task<QueryResult> DaxAsync(string dax, CancellationToken ct = default) =>
        Task.FromResult(Dax?.Invoke(dax) ?? throw new InvalidOperationException("No model."));
    public void ShowDax(string dax) { }
    public void ShowView(string view) { }

    public IPluginStorage Storage(string pluginId) => throw new NotSupportedException("Model Summary keeps no settings.");
    public Task<string?> MicrosoftTokenAsync(string[] scopes, bool interactive, string? clientId = null, string authority = "organizations",
        string? loginHint = null, CancellationToken ct = default) => Task.FromResult<string?>(null);
    public PrivacySettings Privacy => new();
    public Pseudonymizer NewPseudonymizer() => throw new NotSupportedException("Model Summary hands agents no personal data.");
    public bool SetPrivacy(string mode, string? presidioUrl, string language = "en") => false;
    public AgentEndpoint Agent => new("http://127.0.0.1:0/mcp", "", false, "", "");

    public void Log(string title, string detail = "", bool error = false, bool byAgent = false) => Logged.Add(title);
    public void SetStatus(string text) { }
    public void ShowPluginView(string pluginId, string viewId) { }
}

The tests

using System.Text.Json;
using OpenBI.Studio.Core.Engine;
using Xunit;

namespace ModelSummary.Tests;

public sealed class SummaryToolsTests
{
    [Fact]
    public void Folders_says_so_when_no_project_is_open() =>
        Assert.Equal("No project is open in OpenBI Studio.", new SummaryTools(new FakeHost()).Folders());

    [Fact]
    public async Task Top_ranks_by_the_models_measure_and_says_so_in_the_Activity_panel()
    {
        string? asked = null;
        var host = new FakeHost
        {
            Dax = dax =>
            {
                asked = dax;
                return new QueryResult
                {
                    Columns = [new ResultColumn("Products[Name]", "string"), new ResultColumn("[Value]", "double")],
                    Rows =
                    [
                        [JsonSerializer.SerializeToElement("Pale ale"), JsonSerializer.SerializeToElement(1200)],
                        [JsonSerializer.SerializeToElement("Stout"), JsonSerializer.SerializeToElement(800)],
                    ],
                };
            },
        };

        var text = await new SummaryTools(host).Top("Products", "Name", "Revenue", top: 2);

        Assert.Contains("[Revenue]", asked);
        Assert.Contains("TOPN ( 2,", asked);
        Assert.Contains("- Pale ale: 1200", text);
        Assert.Contains("- Stout: 800", text);
        Assert.Contains("Model Summary ranked Name", host.Logged);
    }
}

The test project

The SDK’s package carries OpenBI.Studio.Core.dll as a reference assembly: a test compiles against it, but .NET doesn’t run one (“Reference assemblies cannot be loaded for execution”). A test that makes the studio’s own types, a QueryResult as above, a TableInfo or a Pseudonymizer, references an installed OpenBI Studio’s assemblies instead, on Windows or a Mac:

<Project Sdk="Microsoft.NET.Sdk">

  <!-- Model Summary's tests (docs/sdk/testing/). They make the studio's own types (QueryResult, ResultColumn), so they run
       against an installed OpenBI Studio's assemblies: the SDK's package carries OpenBI.Studio.Core as a reference assembly,
       which .NET compiles against but does not run. Windows or a Mac. -->
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <IsPackable>false</IsPackable>
    <!-- Windows; on a Mac: /Applications/OpenBI Studio.app/Contents/MacOS -->
    <OpenBIStudio>C:\Program Files\OpenBI</OpenBIStudio>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
    <PackageReference Include="ModelContextProtocol" Version="2.2.0" />
    <Reference Include="$(OpenBIStudio)\OpenBI.Studio.Sdk.dll" />
    <Reference Include="$(OpenBIStudio)\OpenBI.Studio.Core.dll" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\ModelSummary\ModelSummary.csproj" />
  </ItemGroup>

</Project>

Model Summary’s own project references the SDK’s package with PrivateAssets="all", so the test project sees only the studio’s assemblies. Then:

cd ModelSummary.Tests
dotnet test

On Linux and in CI

A test that uses only your plug-in’s own types and the SDK’s interfaces runs with the package anywhere, Linux and CI included. To keep most tests there, put the plug-in’s logic in classes of your own that take plain values, and keep the studio’s types at the edges, in the tools.

Last updated · OpenBI Studio 0.3.74