Files

14 KiB

RuleEngineTests Documentation

Comprehensive unit tests for Strata.Rules based on Microsoft RulesEngine patterns. This test suite demonstrates rule creation, evaluation, and various expression types.

Test Structure Overview

graph TD
    Tests[RuleEngineTests]
    Tests --> Basic[Basic Comparison Tests]
    Tests --> Logical[Logical Operator Tests]
    Tests --> Multi[Multiple Property Access Tests]
    Tests --> Groups[Rule Group Tests]
    Tests --> Discount[Discount Calculation Examples]
    Tests --> String[String Operations]
    Tests --> Nested[Nested Properties]
    Tests --> Isolation[RuleSet Isolation Tests]
    Tests --> Complex[Complex Real-World Scenarios]

    Basic --> Equality[Equality ==]
    Basic --> NotEqual[Not Equal !=]
    Basic --> GreaterThan[Greater Than >]
    Basic --> LessThan[Less Than <]

    Logical --> And[AND &]
    Logical --> Or[OR |]
    Logical --> Mixed[Mixed Operators]

    Groups --> AndGroup[AND Group]
    Groups --> OrGroup[OR Group]

    style Tests fill:#e1f5ff
    style Basic fill:#fff4e6
    style Logical fill:#e8f5e9
    style Groups fill:#f3e5f5
    style Discount fill:#fff9c4
    style Complex fill:#ffebee

Test Categories

1. Basic Comparison Tests

Tests fundamental comparison operations with single conditions.

sequenceDiagram
    participant Test
    participant Parameter
    participant Expression
    participant RuleSet
    participant Engine

    Test->>Parameter: Create Parameter("input")
    Test->>Parameter: Get Property("country")
    Test->>Expression: Create Equality (property == "india")
    Test->>RuleSet: Create SingleRule + GroupAnd
    Test->>Engine: RunRules(ruleSet, input)
    Engine-->>Test: Return true/false

Example:

// Test: input.country == "india"
var inputParam = new Parameter("input");
var expression = inputParam.Property("country") == "india";
var rule = new SingleRule("CheckCountryIsIndia", expression);
var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid());

var result = await engine.RunRules(ruleSet, new { country = "india" });
// Result: true

2. Logical Operator Tests

Demonstrates combining expressions using AND (&) and OR (|) operators.

graph LR
    A[Country == #quot;india#quot;] -->|&| C[AND Expression]
    B[LoyaltyFactor > 2] -->|&| C
    C -->|Result| D[Combined Rule]

    E[Country == #quot;india#quot;] -->|#124;| G[OR Expression]
    F[Country == #quot;usa#quot;] -->|#124;| G
    G -->|Result| H[Alternative Rule]

    style C fill:#e8f5e9
    style G fill:#fff4e6

AND Example:

// Rule: input.country == "india" AND input.loyaltyFactor > 2
var countryExpr = inputParam.Property("country") == "india";
var loyaltyExpr = new GreaterThan(inputParam.Property("loyaltyFactor"), 2m);
var expression = countryExpr & loyaltyExpr;

OR Example:

// Rule: input.country == "india" OR input.country == "usa"
var indiaExpr = inputParam.Property("country") == "india";
var usaExpr = inputParam.Property("country") == "usa";
var expression = indiaExpr | usaExpr;

Mixed Operators Example:

// Rule: (country == "india" AND loyaltyFactor >= 2) OR vipCustomer == true
var And = countryExpr & loyaltyExpr;
var expression = And | vipExpr;

3. Multiple Property Access Tests

Tests rules that evaluate multiple properties from the same input object.

graph TD
    Input[Input Object] --> P1[country]
    Input --> P2[totalPurchases]
    Input --> P3[totalOrders]

    P1 -->|==| E1[#quot;india#quot;]
    P2 -->|>=| E2[5000]
    P3 -->|>| E3[2]

    E1 -->|&| Combine[Combined Expression]
    E2 -->|&| Combine
    E3 -->|&| Combine

    Combine --> Result[Evaluation Result]

    style Input fill:#e1f5ff
    style Combine fill:#e8f5e9
    style Result fill:#fff4e6

Example:

// Rule: country == "india" AND totalPurchases >= 5000 AND totalOrders > 2
var expression =
    (inputParam.Property("country") == "india") &
    new GreaterThan(inputParam.Property("totalPurchases"), 4999m) &
    new GreaterThan(inputParam.Property("totalOrders"), 2m);

4. Rule Group Tests

Demonstrates organizing multiple rules into AND or OR groups.

graph TD
    subgraph AND Group [All Rules Must Pass]
        R1[Rule 1: Age > 18]
        R2[Rule 2: Status == Active]
        R3[Rule 3: Balance > 0]
        R1 --> AndEval[AND Evaluation]
        R2 --> AndEval
        R3 --> AndEval
    end

    subgraph OR Group [Any Rule Can Pass]
        R4[Rule 4: Premium Member]
        R5[Rule 5: Loyalty > 5 years]
        R6[Rule 6: Total Spent > $10k]
        R4 --> OrEval[OR Evaluation]
        R5 --> OrEval
        R6 --> OrEval
    end

    AndEval --> Final[Final Result]
    OrEval --> Final

    style AndEval fill:#e8f5e9
    style OrEval fill:#fff4e6

AND Group Example:

// All rules must pass
var rule1 = new SingleRule("R1", inputParam.Property("age") > 18m);
var rule2 = new SingleRule("R2", inputParam.Property("status") == "Active");
var andGroup = new GroupAnd(new[] { rule1, rule2 });
var ruleSet = new RuleSet(andGroup, Guid.NewGuid());

OR Group Example:

// Any rule can pass
var rule1 = new SingleRule("R1", inputParam.Property("premium") == "true");
var rule2 = new SingleRule("R2", new GreaterThan(inputParam.Property("loyalty"), 5m));
var orGroup = new GroupOr(new[] { rule1, rule2 });
var ruleSet = new RuleSet(orGroup, Guid.NewGuid());

5. Discount Calculation Examples

Real-world examples based on Microsoft RulesEngine discount calculation scenarios.

flowchart TD
    Start[Customer Input] --> Check1{Country == #quot;india#quot;?}
    Check1 -->|No| NoDiscount[No Discount]
    Check1 -->|Yes| Check2{Loyalty Factor?}

    Check2 -->|== 2| Check3{Purchases &gt;= $5000<br/>AND Orders &gt; 2?}
    Check2 -->|== 3| Check4{Purchases &gt;= $10000?}

    Check3 -->|Yes| Discount10[10% Discount]
    Check3 -->|No| NoDiscount

    Check4 -->|Yes| Discount20[20% Discount]
    Check4 -->|No| Check3

    style Discount10 fill:#c8e6c9
    style Discount20 fill:#81c784
    style NoDiscount fill:#ffcdd2

10% Discount Rule:

// Rule: country == "india" AND loyaltyFactor <= 2
//       AND totalPurchases >= 5000 AND totalOrders > 2
var expression =
    (inputParam.Property("country") == "india") &
    (inputParam.Property("loyaltyFactor") == 2m) &
    new GreaterThan(inputParam.Property("totalPurchasesToDate"), 4999m) &
    new GreaterThan(inputParam.Property("totalOrders"), 2m);

var rule = new SingleRule("GiveDiscount10", expression);

20% Discount Rule:

// Rule: country == "india" AND loyaltyFactor == 3
//       AND totalPurchases >= 10000
var expression =
    (inputParam.Property("country") == "india") &
    (inputParam.Property("loyaltyFactor") == 3m) &
    new GreaterThan(inputParam.Property("totalPurchasesToDate"), 9999m);

var rule = new SingleRule("GiveDiscount20", expression);

Multiple Discount Rules (OR Group):

// Either 10% or 20% discount can apply
var discount10 = new SingleRule("GiveDiscount10", /* 10% expression */);
var discount20 = new SingleRule("GiveDiscount20", /* 20% expression */);
var orGroup = new GroupOr(new IRule[] { discount10, discount20 });

6. String Operations

Tests string comparison operations including case-sensitivity.

graph LR
    A[String Property] --> B{Comparison Type}
    B -->|Case Sensitive| C[Exact Match]
    B -->|StartsWith| D[Prefix Check]
    B -->|Contains| E[Substring Search]
    B -->|EndsWith| F[Suffix Check]

    C --> Result[Boolean Result]
    D --> Result
    E --> Result
    F --> Result

    style A fill:#e1f5ff
    style Result fill:#e8f5e9

Example:

// Case-sensitive string comparison
var expression = inputParam.Property("name") == "John Doe";

// Tests verify exact string matching
var input = new { name = "John Doe" };  // Returns true
var input2 = new { name = "john doe" }; // Returns false

7. Nested Properties

Demonstrates accessing nested object properties.

graph TD
    Root[Root Object] --> Child1[customer]
    Root --> Child2[address]
    Root --> Child3[account]

    Child1 --> CP1[name]
    Child1 --> CP2[age]

    Child2 --> AP1[street]
    Child2 --> AP2[city]
    Child2 --> AP3[country]

    Child3 --> AC1[balance]
    Child3 --> AC2[type]

    AP3 -.->|Property Access| Expr[Expression: address.country == #quot;USA#quot;]

    style Root fill:#e1f5ff
    style Expr fill:#e8f5e9

Example:

// Accessing nested properties: input.address.country
var addressProp = inputParam.Property("address");
var countryProp = addressProp.Property("country");
var expression = countryProp == "USA";

// Test with nested object
var input = new
{
    address = new { country = "USA", city = "New York" }
};

8. RuleSet Isolation Tests

Verifies that multiple RuleSets evaluate independently without interference.

sequenceDiagram
    participant Test
    participant Engine
    participant RuleSet1
    participant RuleSet2

    Test->>Engine: Create Engine
    Test->>RuleSet1: Create RuleSet (Condition A)
    Test->>RuleSet2: Create RuleSet (Condition B)

    Test->>Engine: RunRules(RuleSet1, input)
    Engine->>RuleSet1: Evaluate
    RuleSet1-->>Engine: Result 1
    Engine-->>Test: Result 1

    Test->>Engine: RunRules(RuleSet2, input)
    Engine->>RuleSet2: Evaluate
    RuleSet2-->>Engine: Result 2
    Engine-->>Test: Result 2

    Note over Test,RuleSet2: Each RuleSet evaluates independently

Example:

// RuleSet 1: Check if country is India
var ruleSet1 = new RuleSet(
    new GroupAnd(new[] {
        new SingleRule("R1", inputParam.Property("country") == "india")
    }),
    Guid.NewGuid()
);

// RuleSet 2: Check if country is USA
var ruleSet2 = new RuleSet(
    new GroupAnd(new[] {
        new SingleRule("R2", inputParam.Property("country") == "usa")
    }),
    Guid.NewGuid()
);

// Each evaluates independently
var result1 = await engine.RunRules(ruleSet1, input); // Can be true or false
var result2 = await engine.RunRules(ruleSet2, input); // Independent result

9. Complex Real-World Scenarios

Demonstrates sophisticated business rules combining multiple patterns.

flowchart TD
    Start[Eligibility Check] --> Type{Membership Type?}

    Type -->|Premium| Eligible[Eligible ✓]

    Type -->|Regular| Age{Account Age<br/>&gt; 365 days?}
    Age -->|No| NotEligible[Not Eligible ✗]
    Age -->|Yes| Spent{Total Spent<br/>&gt; $1000?}

    Spent -->|No| NotEligible
    Spent -->|Yes| Reviews{Negative Reviews<br/>== 0?}

    Reviews -->|No| NotEligible
    Reviews -->|Yes| Eligible

    style Eligible fill:#c8e6c9
    style NotEligible fill:#ffcdd2
    style Start fill:#e1f5ff

Complex Business Rule Example:

// Eligible if: (Premium member) OR (Regular member with good history)
var premiumRule = new SingleRule("PremiumMember",
    inputParam.Property("membershipType") == "Premium");

var regularWithHistoryExpr =
    (inputParam.Property("membershipType") == "Regular") &
    new GreaterThan(inputParam.Property("accountAge"), 365m) &
    new GreaterThan(inputParam.Property("totalSpent"), 1000m) &
    (inputParam.Property("negativeReviews") == 0m);

var regularRule = new SingleRule("RegularMemberGoodHistory", regularWithHistoryExpr);

var eligibilityGroup = new GroupOr(new IRule[] { premiumRule, regularRule });
var ruleSet = new RuleSet(eligibilityGroup, Guid.NewGuid());

Rule Evaluation Flow

sequenceDiagram
    participant Input[Input Object]
    participant Param[Parameter]
    participant Prop[Property]
    participant Expr[Expression]
    participant Rule[SingleRule]
    participant Group[Group AND/OR]
    participant Set[RuleSet]
    participant Engine[RuleSetEngine]
    participant MSEngine[Microsoft RulesEngine]

    Input->>Param: Provide data context
    Param->>Prop: Access property chain
    Prop->>Expr: Build expression tree
    Expr->>Rule: Create rule with name
    Rule->>Group: Add to group
    Group->>Set: Build RuleSet
    Set->>Engine: Submit for evaluation
    Engine->>MSEngine: Convert to RulesEngine format
    MSEngine->>Engine: Return evaluation result
    Engine->>Input: Return boolean result

Key Patterns

Pattern 1: Simple Property Comparison

var inputParam = new Parameter("input");
var expression = inputParam.Property("propertyName") == value;
var rule = new SingleRule("RuleName", expression);
var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid());

Pattern 2: Multiple Conditions (AND)

var expr1 = inputParam.Property("prop1") == value1;
var expr2 = inputParam.Property("prop2") > value2;
var combined = expr1 & expr2;

Pattern 3: Alternative Conditions (OR)

var expr1 = inputParam.Property("prop1") == value1;
var expr2 = inputParam.Property("prop2") == value2;
var combined = expr1 | expr2;

Pattern 4: Multiple Rules in Groups

var rule1 = new SingleRule("R1", expression1);
var rule2 = new SingleRule("R2", expression2);

// All must pass
var andGroup = new GroupAnd(new[] { rule1, rule2 });

// Any can pass
var orGroup = new GroupOr(new[] { rule1, rule2 });

Pattern 5: Nested Property Access

var parentProp = inputParam.Property("parent");
var childProp = parentProp.Property("child");
var expression = childProp == value;

Test Execution

All tests are executed asynchronously using NUnit:

[Test]
public async Task TestName()
{
    // Arrange - Set up rules and input
    var expression = /* create expression */;
    var ruleSet = /* create ruleset */;
    var input = /* create input object */;

    // Act - Execute rule engine
    var result = await engine.RunRules(ruleSet, input);

    // Assert - Verify expected outcome
    Assert.That(result, Is.True);
}

Dependencies

  • Strata.Rules: Core rule engine library
  • Strata.Rules.Rule: Rule definitions (SingleRule, RuleSet, IRule)
  • Strata.Rules.Rule.Expression: Expression types (Parameter, Property, Equal, GreaterThan, etc.)
  • Strata.Rules.Rule.Groups: Grouping logic (And, Or)
  • Microsoft RulesEngine: Underlying evaluation engine
  • NUnit: Testing framework

Test Statistics

  • Total Tests: 34
  • Test Categories: 9
  • Success Rate: 100%
  • Execution Time: ~1 second