Skip to content

Commit 2fb0829

Browse files
committed
docs: enhance unit test naming conventions with industry standards
- Add three-part structure (What-When-Then) based on Roy Osherove's standard - Include key properties: what behavior, when it happens, context, why it matters - Add pattern examples by category (success, error, edge cases, state transitions) - Document anti-patterns to avoid (implementation details, vague conditions) - Add best practices section with do's and don'ts - Include quick reference template and checklist - Add references to established conventions (Roy Osherove, BDD, AAA Pattern) - Provide external resources for further reading - Add 'Osherove' to project dictionary for spell checking
1 parent 7486b25 commit 2fb0829

2 files changed

Lines changed: 224 additions & 10 deletions

File tree

docs/contributing/testing/unit-testing.md

Lines changed: 223 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,89 @@ Unit tests should use descriptive, behavior-driven naming with the `it_should_`
44

55
## Naming Convention
66

7-
- **Format**: `it_should_{describe_expected_behavior}`
7+
- **Format**: `it_should_{expected_behavior}_when_{condition}` or `it_should_{expected_behavior}_given_{state}`
88
- **Style**: Use lowercase with underscores, be descriptive and specific
9-
- **Focus**: Describe what the test validates, not just what function it calls
9+
- **Structure**: Follow the three-part pattern (What-When-Then)
10+
11+
### The Three-Part Structure
12+
13+
Every test name should clearly communicate:
14+
15+
1. **What** - The expected behavior or outcome being tested
16+
2. **When** - The triggering condition or scenario (use `when_` or `given_`)
17+
3. **Context** - Implicit from the test module/struct being tested
18+
19+
This follows established conventions from:
20+
21+
- **Roy Osherove's standard**: `UnitOfWork_StateUnderTest_ExpectedBehavior`
22+
- **BDD Given-When-Then**: Behavior-driven development naming
23+
- **AAA Pattern**: Arrange-Act-Assert reflected in test names
24+
25+
### Key Properties of Good Test Names
26+
27+
A well-named test should tell you:
28+
29+
-**What behavior** is being validated (the expected outcome)
30+
-**When it happens** (the triggering condition or preconditions)
31+
-**What's being tested** (implicit from module context, or explicit in name)
32+
-**Why it matters** (clear from the behavior description)
33+
34+
### Guidelines
35+
36+
- **Be specific about conditions**: Use `when_`, `given_`, `with_`, or `for_` to describe the scenario
37+
- **Describe behavior, not implementation**: Focus on what happens, not how
38+
- **Use complete phrases**: Test names can be long - clarity beats brevity
39+
- **Include edge cases explicitly**: `when_input_is_empty`, `when_value_exceeds_limit`
40+
- **Describe error conditions clearly**: `when_file_not_found`, `given_invalid_format`
1041

1142
## Examples
1243

13-
### ✅ Good Test Names
44+
### ✅ Good Test Names (Following Three-Part Structure)
1445

1546
```rust
1647
#[test]
17-
fn it_should_create_ansible_host_with_valid_ipv4() {
48+
fn it_should_create_valid_host_when_given_ipv4_address() {
49+
// What: create valid host | When: given IPv4 address
1850
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
1951
let host = AnsibleHost::new(ip);
2052
assert_eq!(host.as_ip_addr(), &ip);
2153
}
2254

2355
#[test]
24-
fn it_should_fail_with_invalid_ip_address() {
56+
fn it_should_return_error_when_parsing_invalid_ip_string() {
57+
// What: return error | When: parsing invalid IP string
2558
let result = AnsibleHost::from_str("invalid.ip.address");
2659
assert!(result.is_err());
2760
}
2861

2962
#[test]
30-
fn it_should_serialize_to_json() {
63+
fn it_should_serialize_to_json_string_when_valid_host_exists() {
64+
// What: serialize to JSON | When: valid host exists
3165
let host = AnsibleHost::from_str("192.168.1.1").unwrap();
3266
let json = serde_json::to_string(&host).unwrap();
3367
assert_eq!(json, "\"192.168.1.1\"");
3468
}
69+
70+
#[test]
71+
fn it_should_reject_environment_name_when_containing_uppercase_letters() {
72+
// What: reject name | When: containing uppercase
73+
let result = EnvironmentName::new("MyEnv".to_string());
74+
assert!(matches!(result, Err(ValidationError::InvalidCharacters)));
75+
}
76+
77+
#[test]
78+
fn it_should_preserve_order_when_deserializing_from_json() {
79+
// What: preserve order | When: deserializing from JSON
80+
let json = r#"{"first": 1, "second": 2}"#;
81+
let config: Config = serde_json::from_str(json).unwrap();
82+
assert_eq!(config.keys().collect::<Vec<_>>(), vec!["first", "second"]);
83+
}
3584
```
3685

3786
### ❌ Avoid These Test Names
3887

3988
```rust
89+
// ❌ Too generic - doesn't describe behavior or condition
4090
#[test]
4191
fn test_new() { /* ... */ }
4292

@@ -45,11 +95,174 @@ fn test_from_str() { /* ... */ }
4595

4696
#[test]
4797
fn test_serialization() { /* ... */ }
98+
99+
// ❌ Focuses on implementation, not behavior
100+
#[test]
101+
fn it_should_call_validate_method() { /* ... */ }
102+
103+
#[test]
104+
fn it_should_use_serde_deserialize() { /* ... */ }
105+
106+
// ❌ Missing condition/scenario context
107+
#[test]
108+
fn it_should_fail() { /* What fails? When? Why? */ }
109+
110+
#[test]
111+
fn it_should_return_value() { /* Which value? Under what conditions? */ }
112+
113+
// ❌ Too vague about expected behavior
114+
#[test]
115+
fn it_should_work_correctly() { /* What does "work correctly" mean? */ }
116+
117+
#[test]
118+
fn it_should_handle_edge_case() { /* Which edge case? How is it handled? */ }
119+
```
120+
121+
### Pattern Examples by Category
122+
123+
#### Success Cases
124+
125+
```rust
126+
it_should_create_environment_when_given_valid_name()
127+
it_should_return_formatted_output_when_data_is_complete()
128+
it_should_preserve_state_when_serializing_and_deserializing()
129+
```
130+
131+
#### Error Cases
132+
133+
```rust
134+
it_should_return_error_when_file_does_not_exist()
135+
it_should_reject_config_when_required_field_is_missing()
136+
it_should_fail_validation_when_port_exceeds_maximum()
137+
```
138+
139+
#### Edge Cases
140+
141+
```rust
142+
it_should_handle_empty_string_when_parsing_optional_field()
143+
it_should_return_default_when_environment_variable_is_unset()
144+
it_should_allow_maximum_length_when_validating_string_input()
145+
```
146+
147+
#### State Transitions
148+
149+
```rust
150+
it_should_transition_to_active_when_provisioning_completes()
151+
it_should_remain_in_pending_state_when_validation_fails()
152+
it_should_rollback_to_previous_state_when_operation_errors()
153+
```
154+
155+
## Common Anti-Patterns to Avoid
156+
157+
### ❌ Testing Implementation Details
158+
159+
```rust
160+
// Bad: Tests how something is done
161+
it_should_call_repository_save_method()
162+
163+
// Good: Tests what happens
164+
it_should_persist_environment_when_creation_succeeds()
165+
```
166+
167+
### ❌ Vague Conditions
168+
169+
```rust
170+
// Bad: Unclear when this happens
171+
it_should_fail_validation()
172+
173+
// Good: Specific condition
174+
it_should_fail_validation_when_port_number_is_negative()
48175
```
49176

177+
### ❌ Missing Expected Outcome
178+
179+
```rust
180+
// Bad: Doesn't state what happens
181+
it_should_process_input_when_valid()
182+
183+
// Good: Clear outcome
184+
it_should_return_parsed_config_when_input_is_valid_json()
185+
```
186+
187+
### ❌ Technical Jargon Without Context
188+
189+
```rust
190+
// Bad: Requires domain knowledge to understand
191+
it_should_deserialize_dto()
192+
193+
// Good: Explains the behavior
194+
it_should_convert_json_to_environment_config_when_deserializing()
195+
```
196+
197+
## Best Practices
198+
199+
### Do's ✅
200+
201+
- **Use complete phrases**: `when_given_empty_string` not `when_empty`
202+
- **Be explicit about data states**: `when_file_does_not_exist` not `when_no_file`
203+
- **Describe outcomes clearly**: `should_return_error` not `should_fail`
204+
- **Include relevant values**: `when_port_exceeds_65535` not `when_port_too_large`
205+
- **Name error types**: `should_return_validation_error` not `should_return_error`
206+
207+
### Don'ts ❌
208+
209+
- **Don't test methods directly**: Test behaviors, not function names
210+
- **Don't use abbreviations**: `when_cfg_invalid``when_configuration_is_invalid`
211+
- **Don't skip the condition**: Always include the `when_` or `given_` clause
212+
- **Don't be overly technical**: Focus on business behavior, not implementation
213+
- **Don't make assumptions**: Be explicit about preconditions
214+
215+
## Reading Your Test Names
216+
217+
A good test name should read naturally as a sentence when you add spaces:
218+
219+
-`test_parse_error` → "test parse error" (unclear)
220+
-`it_should_return_error_when_parsing_invalid_json` → "it should return error when parsing invalid JSON" (clear)
221+
50222
## Benefits
51223

52-
- **Clarity**: Test names clearly describe the expected behavior
53-
- **Documentation**: Tests serve as living documentation of the code's behavior
54-
- **BDD Style**: Follows Behavior-Driven Development naming conventions
55-
- **Maintainability**: Easier to understand test failures and purpose
224+
- **Clarity**: Test names clearly describe the expected behavior and conditions
225+
- **Documentation**: Tests serve as living, executable specifications of behavior
226+
- **BDD Style**: Follows established Behavior-Driven Development conventions
227+
- **Maintainability**: Easier to understand test failures and identify affected behavior
228+
- **Traceability**: Clear mapping between requirements/behaviors and tests
229+
- **Debugging**: Failed test names immediately tell you what broke and under what conditions
230+
- **Code Reviews**: Reviewers can understand test purpose without reading implementation
231+
232+
## Quick Reference
233+
234+
### Test Name Template
235+
236+
```rust
237+
it_should_{expected_behavior}_when_{triggering_condition}
238+
it_should_{expected_behavior}_given_{initial_state}
239+
```
240+
241+
### Checklist for Good Test Names
242+
243+
- [ ] Describes the **expected behavior** clearly
244+
- [ ] Specifies the **triggering condition** or scenario
245+
- [ ] Uses complete phrases, not abbreviations
246+
- [ ] Reads naturally as a sentence
247+
- [ ] Focuses on **what** happens, not **how**
248+
- [ ] Includes specific values or states when relevant
249+
- [ ] Is specific enough to understand without reading the test body
250+
251+
## References and Further Reading
252+
253+
This guide is based on established testing conventions and best practices:
254+
255+
- **Roy Osherove's Naming Standard**: "The Art of Unit Testing" - The three-part naming pattern: `UnitOfWork_StateUnderTest_ExpectedBehavior`
256+
- **BDD (Behavior-Driven Development)**: Dan North's Given-When-Then pattern for describing behavior
257+
- **AAA Pattern**: Arrange-Act-Assert structure reflected in test organization and naming
258+
- **Google Testing Blog**: Best practices for test naming and structure
259+
- **Martin Fowler**: Testing patterns and behavior-focused naming
260+
- **Kent Beck**: Test-Driven Development principles
261+
262+
### External Resources
263+
264+
- [The Art of Unit Testing (Roy Osherove)](https://www.artofunittesting.com/)
265+
- [BDD Fundamentals (Dan North)](https://dannorth.net/introducing-bdd/)
266+
- [Google Testing Blog](https://testing.googleblog.com/)
267+
- [xUnit Test Patterns](http://xunitpatterns.com/)
268+
- [Growing Object-Oriented Software, Guided by Tests](http://www.growing-object-oriented-software.com/)

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ nslookup
110110
nullglob
111111
OAAAAN
112112
oneline
113+
Osherove
113114
pacman
114115
parameterizing
115116
parseable

0 commit comments

Comments
 (0)