Testing Event Logic

Event logic is easiest to verify with focused tests around entity state changes and save behavior.

  • Examples below are using xUnit.
  • Build entities with an EventContext.
  • Assert behavior by creating entities, setting properties, and deleting.
  • Keep each test focused on one rule.

Example: computed value

[Fact]
public void ComputeSellableOption_ShouldDefaultToSellable_WhenOperationIsNotMain()
{
    var product = new Product(context)
    {
        Sku = "TEST-PRODUCT",
        SalesDescription = "Test Product"
    };

    var boxUom = new ProductUom(context)
    {
        Product = product,
        Name = "box",
        Operation = UomOperation.Multiply,
        Factor = 12
    };

    Assert.Equal(SellableOption.Sellable, boxUom.SellableOption);
}

Example: seeding entities and calling SaveChanges()

For tests that require persisted entities—such as those that look up related data or trigger logic on save—create the setup entities first and call SaveChanges() before the main assertion.

When multiple tests share the same setup, declare seed data as fields so it runs once per test class construction:

public class ProductUomPropertyTests(EventContext context, IUserPromptHandler prompts)
{
    [Fact]
    public void ValidateSellableOption_ShouldNormalizeDefaults_WhenSettingAnotherDefaultSellingUnit()
    {
        var product = new Product(context)
        {
            Sku = "TEST-PRODUCT",
            SalesDescription = "Test Product"
        };

        var boxUom = new ProductUom(context)
        {
            Product = product,
            Name = "box",
            Operation = UomOperation.Multiply,
            Factor = 12,
            SellableOption = SellableOption.DefaultSellingUnit
        };

        var caseUom = new ProductUom(context)
        {
            Product = product,
            Name = "case",
            Operation = UomOperation.Multiply,
            Factor = 24,
            SellableOption = SellableOption.DefaultSellingUnit
        };

        var palletUom = new ProductUom(context)
        {
            Product = product,
            Name = "pallet",
            Operation = UomOperation.Multiply,
            Factor = 144,
            SellableOption = SellableOption.Sellable
        };

        context.SaveChanges();

        palletUom.SellableOption = SellableOption.DefaultSellingUnit;
        context.SaveChanges();

        Assert.Equal(SellableOption.Sellable, boxUom.SellableOption);
        Assert.Equal(SellableOption.Sellable, caseUom.SellableOption);
        Assert.Equal(SellableOption.DefaultSellingUnit, palletUom.SellableOption);
    }
}

You can also extract common entity creation into helper methods when the same structure is reused across multiple tests:

private Product CreateProduct(string sku = "TEST-PRODUCT")
{
    var product = new Product(context)
    {
        Sku = sku,
        SalesDescription = "Test Product"
    };
    context.SaveChanges();
    return product;
}

private ProductUom AddUom(Product product, string name, UomOperation operation, decimal factor)
{
    var uom = new ProductUom(context)
    {
        Product = product,
        Name = name,
        Operation = operation,
        Factor = factor
    };
    context.SaveChanges();
    return uom;
}

Example: testing for messages

Inject IUserPromptHandler prompts in your test class constructor and assert on prompts.Messages after the action that should produce a user-facing message:

[Fact]
public void ValidateName_ShouldRejectEmpty_WhenUomIsNotMain()
{
    var product = new Product(context)
    {
        Sku = "TEST-PRODUCT",
        SalesDescription = "Test Product"
    };

    var boxUom = new ProductUom(context)
    {
        Product = product,
        Operation = UomOperation.Multiply,
        Factor = 12,
        SellableOption = SellableOption.Sellable
    };

    boxUom.Name = string.Empty;

    Assert.Contains(prompts.Messages,
        m => m.Text == "Name must be specified unless the operation type is Main");
}

Example: using [Theory]

Use [Theory] with [InlineData] when the same logic should hold across multiple input combinations:

[Theory]
[InlineData(UomOperation.Main, 12, true)]
[InlineData(UomOperation.Main, 1, false)]
[InlineData(UomOperation.Multiply, 12, false)]
public void ValidateFactor_ShouldRejectInvalidMainFactor_WhenOperationAndFactorVary(
    UomOperation operation, decimal factor, bool shouldHaveMessage)
{
    var productUom = new ProductUom(context)
    {
        Name = "box",
        Operation = operation,
        SellableOption = SellableOption.Sellable
    };

    productUom.Factor = factor;

    var hasMessage = prompts.Messages.Any(m =>
        m.Text.Contains("is a main unit and its factor should always be one"));

    Assert.Equal(shouldHaveMessage, hasMessage);
}

Example: verify guarded property behavior

[Fact]
public void ValidateOperation_ShouldRejectChangingFromMain_WhenMainUnitExists()
{
    var product = new Product(context)
    {
        Sku = "TEST-PRODUCT",
        SalesDescription = "Test Product"
    };

    var mainUom = product.Uoms.Single(u => u.Operation == UomOperation.Main);
    mainUom.Operation = UomOperation.Multiply;

    Assert.Contains(prompts.Messages, m => m.Text == "Main unit of measure cannot be changed");
}

Test file structure and class names

  • There should be a test project for each module (business logic project).
  • Use the same feature folder structure in your test project as in the logic project.
  • The test class name should match the logic class name with a suffix of Tests. For example, if the logic class name is ProductUomBL, then the test class name should be ProductUomBLTests.
  • Test method names should follow the pattern LogicMethodName_ShouldExpectedBehavior_WhenStateUnderTest. For example: ValidateName_ShouldRejectEmpty_WhenUomIsNotMain or ValidateFactor_ShouldRejectInvalidMainFactor_WhenOperationAndFactorVary.

Test tips

  • You must call context.SaveChanges() before getting entities with context.GetEntities<MyEntity>().
  • To assert user-facing prompts/messages, inject IUserPromptHandler userPrompt in your test class constructor and assert on userPrompt.Messages.
  • Do not create tests for logic the Benevia Core platform provides such as the built-in validation rules ([Required], cascade delete, etc.) or the dirty tracking behavior of Compute rules. Focus on testing your custom logic.
  • Verify both positive and negative paths.
  • Avoid creating unit tests for simple computes or validations that just repeat the same logic as the rule. For example, if you have a compute that calculates Total = Quantity * UnitPrice, you do not need to create a test that asserts Total is correct based on different Quantity and UnitPrice values.