Testing Event Logic
Event logic is easiest to verify with focused tests around entity state changes and save behavior.
Recommended approach
- 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 isProductUomBL, then the test class name should beProductUomBLTests. - Test method names should follow the pattern
LogicMethodName_ShouldExpectedBehavior_WhenStateUnderTest. For example:ValidateName_ShouldRejectEmpty_WhenUomIsNotMainorValidateFactor_ShouldRejectInvalidMainFactor_WhenOperationAndFactorVary.
Test tips
- You must call
context.SaveChanges()before getting entities withcontext.GetEntities<MyEntity>(). - To assert user-facing prompts/messages, inject
IUserPromptHandler userPromptin your test class constructor and assert onuserPrompt.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 ofComputerules. 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 assertsTotalis correct based on differentQuantityandUnitPricevalues.