chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
# 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
|
||||
|
||||
```mermaid
|
||||
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.
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
```csharp
|
||||
// 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:**
|
||||
```csharp
|
||||
// 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:**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
```csharp
|
||||
// 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:**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Customer Input] --> Check1{Country == #quot;india#quot;?}
|
||||
Check1 -->|No| NoDiscount[No Discount]
|
||||
Check1 -->|Yes| Check2{Loyalty Factor?}
|
||||
|
||||
Check2 -->|== 2| Check3{Purchases >= $5000<br/>AND Orders > 2?}
|
||||
Check2 -->|== 3| Check4{Purchases >= $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:**
|
||||
```csharp
|
||||
// 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:**
|
||||
```csharp
|
||||
// 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):**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
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:**
|
||||
```csharp
|
||||
// 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.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Eligibility Check] --> Type{Membership Type?}
|
||||
|
||||
Type -->|Premium| Eligible[Eligible ✓]
|
||||
|
||||
Type -->|Regular| Age{Account Age<br/>> 365 days?}
|
||||
Age -->|No| NotEligible[Not Eligible ✗]
|
||||
Age -->|Yes| Spent{Total Spent<br/>> $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:**
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
```csharp
|
||||
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)
|
||||
```csharp
|
||||
var expr1 = inputParam.Property("prop1") == value1;
|
||||
var expr2 = inputParam.Property("prop2") > value2;
|
||||
var combined = expr1 & expr2;
|
||||
```
|
||||
|
||||
### Pattern 3: Alternative Conditions (OR)
|
||||
```csharp
|
||||
var expr1 = inputParam.Property("prop1") == value1;
|
||||
var expr2 = inputParam.Property("prop2") == value2;
|
||||
var combined = expr1 | expr2;
|
||||
```
|
||||
|
||||
### Pattern 4: Multiple Rules in Groups
|
||||
```csharp
|
||||
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
|
||||
```csharp
|
||||
var parentProp = inputParam.Property("parent");
|
||||
var childProp = parentProp.Property("child");
|
||||
var expression = childProp == value;
|
||||
```
|
||||
|
||||
## Test Execution
|
||||
|
||||
All tests are executed asynchronously using NUnit:
|
||||
|
||||
```csharp
|
||||
[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
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [ClassDiagram.md](../Strata.Rules/ClassDiagram.md) - Expression class hierarchy
|
||||
- [RuleVisitorTests.md](RuleVisitorTests.md) - Visitor pattern tests
|
||||
Reference in New Issue
Block a user