IS4010: AI-Enhanced Application Development

Week 12: Generics, Traits, and Testing

Brandon M. Greenwell

Week 12 Overview

Session 1: Generics and Traits

  • Generic types and functions
  • Trait definitions and implementations
  • Trait bounds and where clauses
  • Common traits: Debug, Clone, Display, Iterator

Session 2: Testing in Rust

  • Test-driven development (TDD)
  • Unit testing with cargo test
  • Integration testing
  • Test organization and best practices

Why Generics and Traits Matter

Real-world impact:

  • Rust standard library: Built on generics (Vec<T>, Option<T>, Result<T, E>)
  • Code reuse: Write once, use with any type
  • Type safety: Compile-time guarantees without runtime cost
  • Zero-cost abstractions: No performance penalty

Industry examples:

  • Servo: Mozilla’s parallel browser engine
  • Tokio: Async runtime using generic futures
  • Serde: Generic serialization framework

Session 1: Generics and Traits

Understanding the Problem

Without generics, you need duplicate code:

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn largest_f64(list: &[f64]) -> &f64 {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

Problem: Same logic, different types → code duplication!

Introducing Generics

Generic functions work with multiple types:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

// Works with any type that can be compared!
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);

let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);

Key concept: T: PartialOrd is a trait bound - T must implement PartialOrd.

📖 Rust Book: Generic Data Types

Generic Structs

Define structs that work with any type:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
}

Multiple type parameters:

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let mixed = Point { x: 5, y: 4.0 };  // T=i32, U=f64
}

📖 Rust Book: Generic Structs

Generic Enums

You’ve been using generic enums all along:

// Option<T> from standard library
enum Option<T> {
    Some(T),
    None,
}

// Result<T, E> for error handling
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Usage:

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

📖 Rust Book: Generic Enums

Generic Methods

Methods on generic structs:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// Method only for specific type
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Notice: impl<T> declares the generic, then Point<T> uses it.

📖 Rust Book: Generic Methods

What Are Traits?

Traits define shared behavior:

pub trait Summary {
    fn summarize(&self) -> String;
}

struct NewsArticle {
    headline: String,
    content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}: {}", self.headline, self.content)
    }
}

Think of traits as: - Interfaces (Java/C#) - Protocols (Swift) - Type classes (Haskell)

📖 Rust Book: Traits

Default Trait Implementations

Traits can provide default behavior:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

struct Tweet {
    username: String,
    content: String,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
    // summarize() uses the default implementation
}

📖 Rust Book: Default Implementations

Traits as Parameters

Accept any type implementing a trait:

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// Longer syntax (trait bound):
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

// Multiple trait bounds:
pub fn notify<T: Summary + Display>(item: &T) {
    // Can call summarize() and also use {} formatting
}

📖 Rust Book: Traits as Parameters

The where Clause

Make complex trait bounds readable:

// Hard to read:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
    // ...
}

// Much clearer with where clause:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // ...
}

Use where when: - Multiple trait bounds - Complex generic relationships - Improves readability

📖 Rust Book: where Clauses

Returning Trait Types

Return types that implement traits:

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("rustacean"),
        content: String::from("Rust is awesome!"),
    }
}

Limitation: Can only return ONE concrete type:

// ❌ ERROR: Can't return different types
fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle { /* ... */ }
    } else {
        Tweet { /* ... */ }  // Error!
    }
}

📖 Rust Book: Returning Traits

Common Standard Library Traits

Frequently used traits you should know:

  • Debug: Format with {:?} (derive with #[derive(Debug)])
  • Clone: Create deep copies with .clone()
  • Copy: Types that can be copied by just copying bits
  • Display: Format with {} (implement manually)
  • PartialEq: Compare with == and !=
  • Eq: Full equivalence relation
  • PartialOrd: Compare with <, >, <=, >=
  • Ord: Total ordering
  • Iterator: Types that can be iterated over

📖 Rust Standard Library Traits

Deriving Traits

Automatically implement common traits:

#[derive(Debug, Clone, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1.clone();

    println!("{:?}", p1);  // Debug
    assert_eq!(p1, p2);    // PartialEq
}

Can derive: - Debug, Clone, Copy - PartialEq, Eq, PartialOrd, Ord - Hash, Default

Cannot derive: Display, Iterator (must implement manually)

📖 Rust Book: Derivable Traits

Implementing Display Trait

Custom formatting for user-facing output:

use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("Point: {}", p);  // Point: (1, 2)
}

📖 Rust Book: Display Trait

Implementing Iterator Trait

Make your types iterable:

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;  // Associated type

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

📖 Rust Book: Iterator Trait

Generic Stack Example

Building a generic data structure:

struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Stack<T> {
        Stack { items: Vec::new() }
    }

    fn push(&mut self, item: T) {
        self.items.push(item);
    }

    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

🎮 Try it in Playground

🤖 AI Prompting: Generics and Traits

Design assistance: - “Help me design a generic cache data structure in Rust” - “What traits should my custom type implement for common operations?” - “Explain when to use trait bounds vs where clauses”

Implementation help: - “How do I implement the Iterator trait for my custom struct?” - “Why can’t I return different types from a function returning impl Trait?” - “Help me fix this trait bound error: [paste error]”

Code review: - “Is this the idiomatic way to use generics in Rust?” - “Should this be a generic function or use dynamic dispatch?” - “How can I make these trait bounds more readable?”

Session 2: Testing in Rust

Why Testing Matters

Industry perspective:

  • Google: “Testing is the key to sustainable software development”
  • Microsoft: 70% of bugs found through testing
  • Rust project: 100,000+ tests in compiler alone

Benefits:

  • Catch bugs early (cheaper to fix)
  • Enable refactoring with confidence
  • Document expected behavior
  • Improve code design

Rust advantage: Excellent built-in testing tools!

Test-Driven Development (TDD)

The TDD cycle:

  1. Red: Write a failing test
  2. Green: Write minimum code to pass
  3. Refactor: Improve code while keeping tests green
// 1. RED: Write test first
#[test]
fn test_stack_push_pop() {
    let mut stack = Stack::new();
    stack.push(42);
    assert_eq!(stack.pop(), Some(42));
}

// 2. GREEN: Implement to pass
impl<T> Stack<T> {
    fn push(&mut self, item: T) { /* implement */ }
    fn pop(&mut self) -> Option<T> { /* implement */ }
}

// 3. REFACTOR: Improve while tests pass

📖 Test-Driven Development by Example

Basic Test Structure

Anatomy of a Rust test:

#[cfg(test)]
mod tests {
    use super::*;  // Import from parent module

    #[test]
    fn test_addition() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }

    #[test]
    fn test_stack_empty() {
        let stack: Stack<i32> = Stack::new();
        assert!(stack.is_empty());
    }

    #[test]
    #[should_panic]
    fn test_divide_by_zero() {
        let _ = 1 / 0;  // Should panic
    }
}

📖 Rust Book: How to Write Tests

Assertion Macros

Built-in assertion tools:

#[test]
fn test_assertions() {
    // Basic assertion
    assert!(true);
    assert!(!false);

    // Equality
    assert_eq!(2 + 2, 4);
    assert_ne!(2 + 2, 5);

    // Custom messages
    let x = 5;
    assert_eq!(x, 5, "x should be 5, but was {}", x);
}

Common patterns:

📖 Rust Book: Checking Results with assert!

Testing Expected Failures

Test that code panics correctly:

#[test]
#[should_panic(expected = "Division by zero")]
fn test_divide_by_zero() {
    divide(10.0, 0.0).unwrap();
}

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

Testing Result errors:

#[test]
fn test_divide_by_zero_result() {
    let result = divide(10.0, 0.0);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err(), "Division by zero");
}

📖 Rust Book: should_panic

Running Tests

Cargo test commands:

# Run all tests
cargo test

# Run tests in parallel (default)
cargo test

# Run tests sequentially
cargo test -- --test-threads=1

# Run specific test
cargo test test_stack_push

# Run tests matching pattern
cargo test stack

# Show println! output
cargo test -- --nocapture

# Run tests and show success output
cargo test -- --show-output

📖 Rust Book: Running Tests

Test Organization

Unit tests (in same file as code):

// src/lib.rs or src/module.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }
}

Integration tests (in tests/ directory):

// tests/integration_test.rs
use my_crate::add;

#[test]
fn test_add_integration() {
    assert_eq!(add(2, 2), 4);
}

📖 Rust Book: Test Organization

Testing Private Functions

Rust allows testing private functions:

fn private_function() -> i32 {
    42
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_private_function() {
        // Can test private functions from test module!
        assert_eq!(private_function(), 42);
    }
}

Philosophy: Tests are part of your code, so they can access private items.

📖 Rust Book: Testing Private Functions

Test Coverage

Measure how much code is tested:

# Install tarpaulin (Linux/macOS)
cargo install cargo-tarpaulin

# Run tests with coverage
cargo tarpaulin --out Html

# Or use llvm-cov (cross-platform)
cargo install cargo-llvm-cov
cargo llvm-cov --html

Industry standards: - 80%+ coverage is good - 90%+ coverage is excellent - 100% coverage is often overkill

🔗 cargo-tarpaulin | 🔗 cargo-llvm-cov

Documentation Tests

Code examples in docs are automatically tested:

/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = my_crate::add(2, 2);
/// assert_eq!(result, 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Run with:

cargo test --doc

Benefits: Ensures documentation examples always work!

📖 Rust Book: Documentation Tests

Benchmark Testing

Measure performance (nightly Rust):

#![feature(test)]
extern crate test;

#[cfg(test)]
mod benches {
    use super::*;
    use test::Bencher;

    #[bench]
    fn bench_add(b: &mut Bencher) {
        b.iter(|| add(2, 2));
    }
}

Alternative: criterion (stable Rust):

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn bench_add(c: &mut Criterion) {
    c.bench_function("add", |b| b.iter(|| add(black_box(2), black_box(2))));
}

🔗 Criterion Documentation

Continuous Integration

GitHub Actions for automatic testing:

name: Rust Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - run: cargo test --verbose
      - run: cargo clippy -- -D warnings
      - run: cargo fmt --check

Benefits: - Tests run on every commit - Catch issues before merge - Enforce code quality standards

🔗 GitHub Actions for Rust

Testing Best Practices

Write good tests:

  1. AAA pattern: Arrange, Act, Assert
  2. One assertion per test (when possible)
  3. Test names describe behavior: test_stack_pop_empty_returns_none
  4. Test edge cases: empty, zero, negative, max values
  5. Fast tests: Slow tests won’t be run often
  6. Deterministic: Same input → same output
  7. Independent: Tests don’t depend on each other
  8. Clear failure messages: Use assert messages

Bad test smells: - Tests that sometimes fail (“flaky tests”) - Tests that depend on external state - Tests that take too long

Test-Driven Stack Implementation

Lab 12 preview - TDD workflow:

// 1. Write test (RED)
#[test]
fn test_new_stack_is_empty() {
    let stack: Stack<i32> = Stack::new();
    assert!(stack.is_empty());
}

// 2. Implement (GREEN)
impl<T> Stack<T> {
    fn new() -> Stack<T> {
        Stack { items: Vec::new() }
    }
    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

// 3. Run tests - should pass!
// 4. Refactor if needed
// 5. Repeat for next feature

🎮 Try in Playground

🤖 AI Prompting: Testing

Test strategy: - “What tests should I write for this generic Stack implementation?” - “Help me design a test suite for error handling in my Result-returning functions” - “What edge cases should I test for this string parsing function?”

Writing tests: - “Generate tests for this function: [paste code]” - “How do I test functions that return Result?” - “Help me write a test for this panic case”

Debugging tests: - “Why is my test failing? [paste test and code]” - “How do I test private functions in Rust?” - “My tests are flaky - what could cause non-deterministic behavior?”

TDD workflow: - “Walk me through implementing this feature using TDD” - “I have this test [paste] - what’s the minimum code to make it pass?”

Real-World Testing Examples

Industry practices:

Rust compiler: - 100,000+ tests - Tests run on every pull request - Multiple test suites (unit, integration, ui tests)

Servo browser engine: - Comprehensive test coverage - Performance benchmarks - Crash testing and fuzzing

Tokio async runtime: - Property-based testing with proptest - Loom for concurrency testing - Miri for undefined behavior detection

Property-Based Testing

Test properties that should always hold:

use quickcheck::quickcheck;

fn reverse<T: Clone>(xs: &[T]) -> Vec<T> {
    let mut rev = vec![];
    for x in xs.iter() {
        rev.insert(0, x.clone())
    }
    rev
}

quickcheck! {
    fn prop_reverse_reverse(xs: Vec<i32>) -> bool {
        xs == reverse(&reverse(&xs))
    }
}

Tools: - quickcheck: Random input generation - proptest: Shrinking failed cases

🔗 Property-Based Testing Introduction

Lab 12 Preview: Generic Stack with TDD

What you’ll build:

  • Generic Stack<T> data structure
  • Implement push, pop, peek, is_empty, len
  • Implement Display and Iterator traits
  • Use TDD: Write tests first, then implementation

Learning goals:

  • Apply generics and traits
  • Practice test-driven development
  • Write comprehensive test suites
  • Understand trait bounds in practice

Deliverable: Fully tested generic stack with 15+ tests!

Career Relevance

Why these skills matter:

Generics: - Foundation of modern language features (Java, C#, TypeScript, Swift) - Essential for library and framework development - Interview questions: “Design a generic data structure”

Traits: - Interface design skills transfer to all languages - Composition over inheritance (modern best practice) - Understanding trait bounds helps with type systems

Testing: - Expected in all professional development - TDD is an industry standard practice - Testing skills demonstrate code quality awareness

Interview examples: - “Explain the difference between generics and dynamic dispatch” - “How would you test this function?” - “Design an interface for [problem] - what methods would it have?”

Key Takeaways

Generics: - Enable code reuse without sacrificing type safety - Zero-cost abstractions - no runtime overhead - Work with structs, enums, functions, and methods

Traits: - Define shared behavior across types - Enable polymorphism without inheritance - Foundation of Rust’s standard library

Testing: - Built-in with cargo test - TDD leads to better design - Comprehensive tests enable confident refactoring

Together: Generics + Traits + Tests = Robust, reusable Rust code

Additional Resources

Official documentation: - The Rust Book - Chapter 10: Generic Types, Traits, and Lifetimes - The Rust Book - Chapter 11: Writing Automated Tests - Rust by Example: Generics - Rust by Example: Testing

Articles and guides: - Rust Traits: A Deep Dive - Effective Testing in Rust - Generic Associated Types

Tools: - Rust Playground - Test code online - cargo-tarpaulin - Code coverage - proptest - Property-based testing

Next Week: Idiomatic Rust

Week 13 topics:

  • Closures and function pointers
  • Iterator patterns and combinators
  • Smart pointers (Box, Rc, RefCell, Arc)
  • Common patterns and idioms
  • Code organization strategies

Why it matters: Move from “it works” to “it’s idiomatic Rust”

Lab 13: Refactor code to use idiomatic Rust patterns

Questions?

Get help:

This week’s materials:

Start Lab 12!

Lab due: Sunday, November 16, 2025, at 11:59 PM

What to do:

  1. Review lecture materials and examples
  2. Work through interactive exercises
  3. Begin Lab 12: Generic Stack implementation
  4. Use TDD - write tests first!
  5. Ask questions early and often

Remember: You’re not just learning Rust - you’re learning universal programming concepts that apply everywhere!

Good luck! 🦀