Skip to main content

expectation_story/
lib.rs

1//! User stories as acceptance tests: [`plan`] compiles stories (and generated coverage
2//! routes) into test plans, and [`report`] holds what happened when they ran.
3
4pub mod plan;
5pub mod report;
6
7pub use plan::{Because, Check, Origin, Plan, PlanStep, Planned, coverage, coverage_plans, plan_stories, untested};
8pub use report::{CheckResult, PlanResult, Report, Status, StepResult, Verdict, load_results, load_results_for, results_file};
9
10#[cfg(test)]
11mod tests {
12    use super::*;
13    use expectation_model::Suite;
14
15    const SUITE: &str = "
16Vocabulary:
17  State: the kettle is off | heating | dry
18  State: the dial is 0..100 %
19  State: the help screen is shown | hidden
20  Event: the user presses reset
21  Event: the user presses help
22  Event: the water runs out
23
24Background:
25  Given the kettle is off
26  And the dial is 0 %
27
28Feature: Heating
29  Expectation: Turning the dial up starts the kettle
30    Given the kettle is off
31    When the dial is above 55 %
32    Then the kettle is heating
33
34  Expectation: Running dry stops the kettle
35    Given the kettle is heating
36    When the water runs out
37    Then the kettle is dry
38
39  Expectation: Reset turns the kettle off
40    When the user presses reset
41    Then the kettle is off
42
43  Expectation: Help opens the help screen
44    Given the help screen is hidden
45    When the user presses help
46    Then the help screen is shown
47";
48
49    fn planned(stories: &str) -> (Suite, expectation_graph::Analysis, Planned) {
50        let src = format!("{SUITE}{stories}");
51        let l = expectation_language::load_sources("t", &[("t.expect", &src)]);
52        assert!(!l.has_errors(), "{}", l.render_diagnostics());
53        let a = expectation_graph::analyze(&l.suite);
54        let p = plan_stories(&l.suite, &a);
55        (l.suite, a, p)
56    }
57
58    fn codes(p: &Planned) -> Vec<&'static str> {
59        p.diagnostics.iter().map(|d| d.code).collect()
60    }
61
62    #[test]
63    fn checks_come_from_the_expectations() {
64        let (s, _, p) = planned(
65            "
66Story: Boil dry
67  When the dial is 80 %
68  When the water runs out
69  Then the kettle is dry
70",
71        );
72        assert!(codes(&p).is_empty(), "{:?}", codes(&p));
73        let plan = &p.plans[0];
74        assert_eq!(plan.start_checks.len(), 2);
75        // The story never says the kettle heats; the expectation does, so the test checks it.
76        let first = &plan.steps[0];
77        assert_eq!(first.fired.len(), 1);
78        assert_eq!(s.vocabulary.describe(&first.state), "the kettle is heating and the dial is 80 %");
79        assert!(matches!(first.checks[0].because, Because::Expectation(_)));
80        assert_eq!(plan.steps[1].checks.len(), 1, "the story's Then duplicates the promise");
81    }
82
83    #[test]
84    fn stories_cant_rely_on_unspecified_or_contradicted_behaviour() {
85        let (_, _, p) = planned("Story: S\n  When the water runs out\n");
86        assert_eq!(codes(&p), vec!["unspecified"]);
87
88        let (_, _, p) = planned("Story: S\n  When the dial is 80 %\n  Then the kettle is off\n");
89        assert_eq!(codes(&p), vec!["contradicts"]);
90
91        // 50..100 % might or might not be above 55 %.
92        let (_, _, p) = planned("Story: S\n  When the dial is at least 50 %\n");
93        assert_eq!(codes(&p), vec!["unclear-step"]);
94
95        // The Background doesn't say whether help is showing.
96        let (_, _, p) = planned("Story: S\n  When the user presses help\n");
97        assert_eq!(codes(&p), vec!["unclear-step"]);
98        let (_, _, p) = planned(
99            "Story: S\n  Given the help screen is hidden\n  When the user presses help\n  Then the dial is 0 %\n",
100        );
101        assert!(codes(&p).is_empty());
102
103        let (_, _, p) = planned("Story: S\n  When the dial is 80 %\n  Then the help screen is hidden\n");
104        assert_eq!(codes(&p), vec!["unpromised"]);
105    }
106
107    #[test]
108    fn coverage_fills_the_gaps() {
109        let (s, a, p) = planned("Story: S\n  When the dial is 80 %\n");
110        let cov = coverage(&s, &p.plans);
111        assert_eq!(cov.iter().filter(|c| c.is_empty()).count(), 3);
112        assert_eq!(untested(&s, &a, &p.plans).len(), 3);
113        let generated = coverage_plans(&s, &a, &p.plans);
114        let titles: Vec<&str> = generated.iter().map(|p| p.title.as_str()).collect();
115        assert_eq!(
116            titles,
117            vec![
118                "Reaching «Running dry stops the kettle»",
119                "Reaching «Reset turns the kettle off»",
120                "Reaching «Help opens the help screen»"
121            ]
122        );
123        // To reach running dry, the generated test starts the kettle first.
124        assert_eq!(generated[0].steps.len(), 2);
125        // Help needs the help screen hidden, which the Background leaves open: it's arranged.
126        assert_eq!(generated[2].given.last().unwrap().text, "the help screen is hidden");
127    }
128}