Files
sql-utilities/tests/Strata.SqlTools.Rules.Tests/RuleEngineTests.md

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