Skip to content

Testing Results

Chris Michael edited this page Sep 29, 2026 · 4 revisions

Pudu

Testing Results

Validators are ordinary values, so tests assert on their results directly. The Testing module runs a validator and narrows its failures by path, message, code, severity, and state:

module Ex08Testing

import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Testing as Testing
import PuduLangValidator.Validator as Validator

type Customer = { name: Str, age: Int }

fn customerValidator() -> Validator.Validator[Customer] {
  let name = Rule.build(Rule.withCode(Rule.withMessage(
    TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)),
    "Name is required."), "NameRequired"))
  let age = Rule.build(Rule.withCode(
    NumberRule.greaterThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 18), "AgeCheck"))
  Validator.add(Validator.add(Validator.create(), name), age)
}

export fn main() -> Int {
  let validator = customerValidator()
  let result = Testing.testValidate(&validator, Customer{name: "", age: 3})
  let nameFails = Testing.forProperty(&result, "name")
  let coded = Testing.withCode(nameFails, "NameRequired")
  let line = "any=" + show(Testing.hasAny(&coded))
    + " none=" + show(Testing.hasNone(&Testing.withCode(Testing.forProperty(&result, "name"), "Other")))
    + " onlyAge=" + show(Testing.onlyErrorsFor(&result, ["name", "age"]))
  match Io.writeLine(line) {
    case Ok(_) => if Testing.hasAny(&coded)
      && Testing.hasErrorFor(&result, "age")
      && Testing.hasCode(&result, "AgeCheck")
      && Testing.onlyErrorsFor(&result, ["name", "age"]) { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

any=true none=true onlyAge=true

Testing.testValidateWith runs with explicit options, and testValidateAsync covers async validators. Each withX filter has a withoutX mirror for asserting absence. hasAny and hasNone ask whether a query matched; only asks whether the query caught every failure; onlyErrorsFor pins down the exact set of failing paths.

The Result module works one level lower: messages joins failure text, forProperty filters by path, toDictionary groups messages by path, and toResult turns a result back into a Result holding the value or its failures.

Reading results back

Result.messages joins failure text, Result.forProperty filters by path, Result.toDictionary groups messages by path, and Result.toResult turns a result back into a Result holding the value or its failures. Result.fromFailures rebuilds a result from failures alone, which is how custom checks and prevalidation answer early. On the Testing side, testValidateWith runs with explicit options, hasNoErrorFor asserts a clean path, the withoutX mirrors assert absence, and only asks whether a query caught every failure:

module Ex13Result

import Std.Io as Io
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Testing as Testing
import PuduLangValidator.Validator as Validator

type Customer = { name: Str, age: Int }

fn customerValidator() -> Validator.Validator[Customer] {
  let name = Rule.build(TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)))
  let age = Rule.build(NumberRule.greaterThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 18))
  Validator.add(Validator.add(Validator.create(), name), age)
}

export fn main() -> Int {
  let validator = customerValidator()
  let bad = Customer{name: "", age: 3}
  let result = Validator.validate(&validator, bad)
  let named = Validation.forProperty(&result, "name")
  let catalog = Validation.toDictionary(&result)
  let back = Validation.toResult(&result, bad)
  let rebuilt = Validation.fromFailures(result.errors)
  let pristine = Validation.fromFailures([])
  let chosen = Testing.testValidateWith(&validator, bad,
    Validator.includeProperties(Validator.defaults(), ["age"]))
  let single = Validator.validate(&validator, Customer{name: "Bo", age: 3})
  let line = "joined=" + show(Validation.messages(&result, "|").contains("|"))
    + " nameFails=" + show(named.length()) + " paths=" + show(catalog.size())
    + " backFails=" + show(match back { case Ok(_) => 0 case Err(errors) => errors.length() })
    + " rebuilt=" + show(rebuilt.isValid) + " pristine=" + show(pristine.isValid)
    + " chosen=" + show(chosen.errors.length())
    + " cleanAge=" + show(Testing.hasNoErrorFor(&result, "age"))
    + " cleanZed=" + show(Testing.hasNoErrorFor(&result, "zzz"))
    + " narrowed=" + show(Testing.hasNone(&Testing.withoutCode(Testing.forProperty(&result, "name"), "NotEmpty")))
    + " all=" + show(Testing.only(&Testing.forProperty(&result, "name")))
    + " allSingle=" + show(Testing.only(&Testing.forProperty(&single, "age")))
  match Io.writeLine(line) {
    case Ok(_) => if named.length() == 1 && catalog.size() == 2 && !rebuilt.isValid
      && pristine.isValid && chosen.errors.length() == 1 && !Testing.hasNoErrorFor(&result, "age")
      && Testing.hasNoErrorFor(&result, "zzz")
      && !Testing.only(&Testing.forProperty(&result, "name"))
      && Testing.only(&Testing.forProperty(&single, "age")) { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

joined=true nameFails=1 paths=2 backFails=2 rebuilt=false pristine=true chosen=1 cleanAge=false cleanZed=true narrowed=true all=false allSingle=true

The two failure texts join with a separator; one failure sits at name and two paths share the dictionary. Turning the result back yields its two failures, rebuilding from them stays invalid, and rebuilding from nothing stays valid. Selecting age keeps one failure; age is not clean but zzz is. Removing the NotEmpty code empties the name query, which catches one of two failures (all=false) while the single-failure query catches everything (allSingle=true).

Filtering by message, severity, and state

Every withX filter has a withoutX mirror for asserting absence. Testing.withMessage and Testing.withoutMessage narrow by failure text, withSeverity and withoutSeverity by classification, and withState and withoutState by application state:

module Ex20TestFilters

import Std.Io as Io
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Testing as Testing
import PuduLangValidator.Validator as Validator

type Customer = { name: Str, age: Int }

fn customerValidator() -> Validator.Validator[Customer] {
  let name = Rule.build(Rule.withCode(Rule.withMessage(
    TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)),
    "Name is required."), "NameRequired"))
  let age = Rule.build(Rule.withState(Rule.withSeverity(Rule.withCode(
    NumberRule.greaterThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 18),
    "AgeCheck"), Validation.Warning), "minor"))
  Validator.add(Validator.add(Validator.create(), name), age)
}

export fn main() -> Int {
  let validator = customerValidator()
  let result = Testing.testValidate(&validator, Customer{name: "", age: 3})
  let single = Testing.testValidate(&validator, Customer{name: "Bo", age: 3})
  let line = "msgHit=" + show(Testing.hasAny(&Testing.withMessage(Testing.forProperty(&result, "name"), "Name is required.")))
    + " msgGone=" + show(Testing.hasNone(&Testing.withoutMessage(Testing.forProperty(&result, "name"), "Name is required.")))
    + " warnHit=" + show(Testing.hasAny(&Testing.withSeverity(Testing.forProperty(&result, "age"), Validation.Warning)))
    + " warnGone=" + show(Testing.hasNone(&Testing.withoutSeverity(Testing.forProperty(&result, "age"), Validation.Warning)))
    + " stateHit=" + show(Testing.hasAny(&Testing.withState(Testing.forProperty(&result, "age"), "minor")))
    + " stateGone=" + show(Testing.hasNone(&Testing.withoutState(Testing.forProperty(&result, "age"), "minor")))
    + " allSingle=" + show(Testing.only(&Testing.forProperty(&single, "age")))
  match Io.writeLine(line) {
    case Ok(_) => if Testing.hasAny(&Testing.withMessage(Testing.forProperty(&result, "name"), "Name is required."))
      && Testing.hasNone(&Testing.withoutMessage(Testing.forProperty(&result, "name"), "Name is required."))
      && Testing.hasAny(&Testing.withSeverity(Testing.forProperty(&result, "age"), Validation.Warning))
      && Testing.hasNone(&Testing.withoutSeverity(Testing.forProperty(&result, "age"), Validation.Warning))
      && Testing.hasAny(&Testing.withState(Testing.forProperty(&result, "age"), "minor"))
      && Testing.hasNone(&Testing.withoutState(Testing.forProperty(&result, "age"), "minor"))
      && Testing.only(&Testing.forProperty(&single, "age")) { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

msgHit=true msgGone=true warnHit=true warnGone=true stateHit=true stateGone=true allSingle=true

Each filter finds its failure and each mirror confirms the absence. The single-failure query catches everything, so only answers true for it.

Related

Clone this wiki locally