Skip to content

Chapter 18: Testing and Debugging#

In this chapter, we'll explore Jac's built-in testing framework and debugging strategies for spatial applications. We'll build a comprehensive test suite for a social media system that demonstrates testing nodes, edges, walkers, and complex graph operations.

What You'll Learn

  • Jac's built-in testing framework with .test.jac files
  • Testing spatial applications with nodes, edges, and walkers
  • Debugging techniques for graph traversal and walker behavior
  • Performance testing and optimization strategies
  • Test-driven development patterns for OSP

Jac's Built-in Testing Framework#

Jac provides a powerful testing framework that automatically discovers and runs tests. When you run jac test myfile.jac, it automatically looks for myfile.test.jac and executes all test blocks within it.

Testing Framework Benefits

  • Automatic Discovery: .test.jac files are automatically found and executed
  • Graph-Aware Testing: Native support for testing spatial relationships
  • Walker Testing: Test mobile computation patterns naturally
  • Type-Safe Assertions: Leverage Jac's type system in test validation
  • Zero Configuration: No external testing frameworks required

Traditional vs Jac Testing#

Testing Comparison

# test_social_media.py - External framework required
import unittest
from social_media import Profile, Tweet, Comment

class TestSocialMedia(unittest.TestCase):
    def setUp(self):
        self.profile = Profile("test_user")
        self.tweet = Tweet("Hello world!")

    def test_create_profile(self):
        self.assertEqual(self.profile.username, "test_user")
        self.assertIsInstance(self.profile, Profile)

    def test_create_tweet(self):
        self.profile.add_tweet(self.tweet)
        self.assertEqual(len(self.profile.tweets), 1)
        self.assertEqual(self.profile.tweets[0].content, "Hello world!")

    def test_follow_user(self):
        other_user = Profile("other_user")
        self.profile.follow(other_user)
        self.assertIn(other_user, self.profile.following)

if __name__ == '__main__':
    unittest.main()
# social_media.test.jac - Built-in testing

test create_profile {
    root spawn visit_profile();
    profile = [root --> Profile][0];
    check isinstance(profile, Profile);
    check profile.username == "";  # Default value
}

test update_profile {
    root spawn update_profile(new_username="test_user");
    profile = [root --> Profile][0];
    check profile.username == "test_user";
}

test create_tweet {
    root spawn create_tweet(content="Hello world!");
    tweet = [root --> Profile --> Tweet][0];
    check tweet.content == "Hello world!";
    check isinstance(tweet, Tweet);
}

test follow_user {
    # Create another profile to follow
    other_profile = Profile(username="other_user");
    other_profile spawn follow_request();

    # Check follow relationship exists
    followed = [root --> Profile ->:Follow:-> Profile][0];
    check followed.username == "other_user";
}

Testing Graph Structures#

Testing spatial applications requires verifying both node properties and relationship integrity. Let's build a comprehensive social media system and test it thoroughly.

Basic Social Media System#

Social Media Implementation

# social_media.jac
import from datetime { datetime }

node Profile {
    has username: str = "";
    has bio: str = "";
    has follower_count: int = 0;

    can update with update_profile entry;
    can follow with follow_request entry;
    can unfollow with unfollow_request entry;
}

node Tweet {
    has content: str;
    has created_at: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S");
    has like_count: int = 0;

    can update with update_tweet exit;
    can delete with remove_tweet exit;
    can like with like_tweet entry;
    can unlike with unlike_tweet entry;
}

node Comment {
    has content: str;
    has created_at: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S");

    can update with update_comment entry;
    can delete with remove_comment entry;
}

edge Follow {
    has followed_at: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S");
}

edge Post {
    has posted_at: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S");
}

edge Like {
    has liked_at: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S");
}

edge CommentOn {}

walker visit_profile {
    can visit_profile with `root entry {
        visit [-->Profile] else {
            new_profile = here ++> Profile();
            visit new_profile;
        }
    }
}

walker update_profile(visit_profile) {
    has new_username: str;
    has new_bio: str = "";
}

walker follow_request {
    can follow_user with Profile entry {
        current_profile = [root --> Profile][0];
        if current_profile != here {
            current_profile +>:Follow:+> here;
            here.follower_count += 1;
            report {"message": f"Now following {here.username}"};
        } else {
            report {"error": "Cannot follow yourself"};
        }
    }
}

walker unfollow_request {
    can unfollow_user with Profile entry {
        current_profile = [root --> Profile][0];
        follow_edges = [edge current_profile ->:Follow:-> here];
        if follow_edges {
            del follow_edges[0];
            here.follower_count -= 1;
            report {"message": f"Unfollowed {here.username}"};
        } else {
            report {"error": "Not following this user"};
        }
    }
}

walker create_tweet(visit_profile) {
    has content: str;

    can post_tweet with Profile entry {
        tweet = here +>:Post:+> Tweet(content=self.content);
        report {"message": "Tweet created", "tweet_id": tweet[0].id};
    }
}

walker update_tweet {
    has updated_content: str;
}

walker remove_tweet {}

walker like_tweet {
    can like_post with Tweet entry {
        current_profile = [root --> Profile][0];
        existing_likes = [edge current_profile ->:Like:-> here];

        if not existing_likes {
            current_profile +>:Like:+> here;
            here.like_count += 1;
            report {"message": "Tweet liked"};
        } else {
            report {"error": "Already liked this tweet"};
        }
    }
}

walker unlike_tweet {
    can unlike_post with Tweet entry {
        current_profile = [root --> Profile][0];
        like_edges = [edge current_profile ->:Like:-> here];

        if like_edges {
            del like_edges[0];
            here.like_count -= 1;
            report {"message": "Tweet unliked"};
        } else {
            report {"error": "Haven't liked this tweet"};
        }
    }
}

walker comment_on_tweet {
    has content: str;

    can add_comment with Tweet entry {
        current_profile = [root --> Profile][0];
        comment = current_profile ++> Comment(content=self.content);
        here +>:CommentOn:+> comment[0];
        report {"message": "Comment added", "comment_id": comment[0].id};
    }
}
# social_media.py - Manual implementation
from datetime import datetime
from typing import List, Optional

class Profile:
    def __init__(self, username: str = "", bio: str = ""):
        self.username = username
        self.bio = bio
        self.follower_count = 0
        self.following = []
        self.followers = []
        self.tweets = []

    def follow(self, other_profile):
        if other_profile not in self.following and other_profile != self:
            self.following.append(other_profile)
            other_profile.followers.append(self)
            other_profile.follower_count += 1
            return True
        return False

    def unfollow(self, other_profile):
        if other_profile in self.following:
            self.following.remove(other_profile)
            other_profile.followers.remove(self)
            other_profile.follower_count -= 1
            return True
        return False

class Tweet:
    def __init__(self, content: str, author: Profile):
        self.content = content
        self.author = author
        self.created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.like_count = 0
        self.liked_by = []
        self.comments = []

    def like(self, user: Profile):
        if user not in self.liked_by:
            self.liked_by.append(user)
            self.like_count += 1
            return True
        return False

    def unlike(self, user: Profile):
        if user in self.liked_by:
            self.liked_by.remove(user)
            self.like_count -= 1
            return True
        return False

class Comment:
    def __init__(self, content: str, author: Profile, tweet: Tweet):
        self.content = content
        self.author = author
        self.tweet = tweet
        self.created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

Comprehensive Test Suite#

Complete Test Coverage

# social_media.test.jac

# Test basic profile creation and updates
test create_and_update_profile {
    # Test profile creation
    root spawn visit_profile();
    profile = [root --> Profile][0];
    check isinstance(profile, Profile);
    check profile.username == "";
    check profile.follower_count == 0;

    # Test profile update
    root spawn update_profile(
        new_username="alice",
        new_bio="Software developer"
    );
    updated_profile = [root --> Profile][0];
    check updated_profile.username == "alice";
    check updated_profile.bio == "Software developer";
}

# Test following functionality
test follow_and_unfollow_users {
    # Create main user profile
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");

    # Create another user to follow
    bob_profile = Profile(username="bob");

    # Test following
    bob_profile spawn follow_request();
    follow_edge = [root --> Profile ->:Follow:-> Profile][0];
    check follow_edge.username == "bob";
    check bob_profile.follower_count == 1;

    # Test follow edge properties
    follow_edges = [edge [root --> Profile] ->:Follow:-> bob_profile];
    check len(follow_edges) == 1;
    check hasattr(follow_edges[0], "followed_at");

    # Test unfollowing
    bob_profile spawn unfollow_request();
    remaining_follows = [root --> Profile ->:Follow:-> Profile];
    check len(remaining_follows) == 0;
    check bob_profile.follower_count == 0;
}

# Test tweet creation and management
test tweet_lifecycle {
    # Ensure we have a profile
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");

    # Test tweet creation
    root spawn create_tweet(content="Hello world!");
    tweet = [root --> Profile ->:Post:-> Tweet][0];
    check tweet.content == "Hello world!";
    check isinstance(tweet, Tweet);
    check hasattr(tweet, "created_at");

    # Test tweet update
    tweet spawn update_tweet(updated_content="Hello updated world!");
    check tweet.content == "Hello updated world!";

    # Test multiple tweets
    root spawn create_tweet(content="Second tweet");
    all_tweets = [root --> Profile ->:Post:-> Tweet];
    check len(all_tweets) == 2;

    # Test tweet deletion
    tweet spawn remove_tweet();
    remaining_tweets = [root --> Profile ->:Post:-> Tweet];
    check len(remaining_tweets) == 1;
    check remaining_tweets[0].content == "Second tweet";
}

# Test liking functionality
test like_and_unlike_tweets {
    # Setup: Create profile and tweet
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");
    root spawn create_tweet(content="Likeable tweet");

    tweet = [root --> Profile ->:Post:-> Tweet][0];
    check tweet.like_count == 0;

    # Test liking
    tweet spawn like_tweet();
    check tweet.like_count == 1;

    # Verify like relationship exists
    like_edges = [edge [root --> Profile] ->:Like:-> tweet];
    check len(like_edges) == 1;

    # Test double-liking (should fail)
    result = tweet spawn like_tweet();
    check tweet.like_count == 1;  # Should remain 1

    # Test unliking
    tweet spawn unlike_tweet();
    check tweet.like_count == 0;

    # Verify like relationship removed
    remaining_likes = [edge [root --> Profile] ->:Like:-> tweet];
    check len(remaining_likes) == 0;
}

# Test commenting functionality
test comment_system {
    # Setup: Create profile and tweet
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");
    root spawn create_tweet(content="Tweet for comments");

    tweet = [root --> Profile ->:Post:-> Tweet][0];

    # Test commenting
    tweet spawn comment_on_tweet(content="Great tweet!");
    comments = [tweet ->:CommentOn:-> Comment];
    check len(comments) == 1;
    check comments[0].content == "Great tweet!";

    # Test multiple comments
    tweet spawn comment_on_tweet(content="I agree!");
    all_comments = [tweet ->:CommentOn:-> Comment];
    check len(all_comments) == 2;

    # Test comment update
    first_comment = all_comments[0];
    first_comment spawn update_comment(updated_content="Updated comment");
    check first_comment.content == "Updated comment";

    # Test comment deletion
    first_comment spawn remove_comment();
    remaining_comments = [tweet ->:CommentOn:-> Comment];
    check len(remaining_comments) == 1;
}

# Test complex graph relationships
test complex_social_graph {
    # Create multiple users
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");

    bob = Profile(username="bob");
    charlie = Profile(username="charlie");

    # Create follow relationships: alice -> bob -> charlie
    bob spawn follow_request();
    charlie spawn follow_request();  # alice follows charlie too

    # Alice creates a tweet
    root spawn create_tweet(content="Alice's tweet");
    alice_tweet = [root --> Profile ->:Post:-> Tweet][0];

    # Bob likes Alice's tweet
    # (Note: In a real system, you'd switch user context)
    alice_tweet spawn like_tweet();

    # Verify complex relationships
    alice_profile = [root --> Profile][0];
    alice_following = [alice_profile ->:Follow:-> Profile];
    check len(alice_following) == 2;  # follows bob and charlie

    alice_tweets = [alice_profile ->:Post:-> Tweet];
    check len(alice_tweets) == 1;

    tweet_likes = [alice_tweets[0] <-:Like:<- Profile];
    check len(tweet_likes) == 1;  # liked by alice (herself)
}

# Test error conditions and edge cases
test error_conditions {
    # Test operations without profile
    try {
        root spawn create_tweet(content="No profile tweet");
        check False;  # Should not reach here
    } except Exception {
        check True;  # Expected behavior
    }

    # Create profile for other tests
    root spawn visit_profile();
    root spawn update_profile(new_username="test_user");

    # Test self-follow prevention
    alice_profile = [root --> Profile][0];
    result = alice_profile spawn follow_request();

    # Should not create self-follow
    self_follows = [alice_profile ->:Follow:-> alice_profile];
    check len(self_follows) == 0;
}

# Performance and stress testing
test performance_operations {
    # Setup
    root spawn visit_profile();
    root spawn update_profile(new_username="performance_user");

    # Create multiple tweets quickly
    for i in range(10) {
        root spawn create_tweet(content=f"Tweet number {i}");
    }

    all_tweets = [root --> Profile ->:Post:-> Tweet];
    check len(all_tweets) == 10;

    # Like all tweets
    for tweet in all_tweets {
        tweet spawn like_tweet();
    }

    # Verify all likes
    for tweet in all_tweets {
        check tweet.like_count == 1;
    }

    # Test batch operations work correctly
    total_likes = sum([tweet.like_count for tweet in all_tweets]);
    check total_likes == 10;
}
# test_social_media.py
import unittest
from social_media import Profile, Tweet, Comment

class TestSocialMedia(unittest.TestCase):
    def setUp(self):
        self.alice = Profile("alice", "Software developer")
        self.bob = Profile("bob", "Designer")

    def test_create_and_update_profile(self):
        profile = Profile()
        self.assertEqual(profile.username, "")
        self.assertEqual(profile.follower_count, 0)

        profile.username = "alice"
        profile.bio = "Software developer"
        self.assertEqual(profile.username, "alice")

    def test_follow_and_unfollow_users(self):
        # Test following
        success = self.alice.follow(self.bob)
        self.assertTrue(success)
        self.assertIn(self.bob, self.alice.following)
        self.assertEqual(self.bob.follower_count, 1)

        # Test unfollowing
        success = self.alice.unfollow(self.bob)
        self.assertTrue(success)
        self.assertNotIn(self.bob, self.alice.following)
        self.assertEqual(self.bob.follower_count, 0)

    def test_tweet_lifecycle(self):
        tweet = Tweet("Hello world!", self.alice)
        self.alice.tweets.append(tweet)

        self.assertEqual(tweet.content, "Hello world!")
        self.assertEqual(len(self.alice.tweets), 1)

        # Update tweet
        tweet.content = "Hello updated world!"
        self.assertEqual(tweet.content, "Hello updated world!")

    def test_like_and_unlike_tweets(self):
        tweet = Tweet("Likeable tweet", self.alice)

        # Test liking
        success = tweet.like(self.bob)
        self.assertTrue(success)
        self.assertEqual(tweet.like_count, 1)

        # Test double-liking
        success = tweet.like(self.bob)
        self.assertFalse(success)
        self.assertEqual(tweet.like_count, 1)

        # Test unliking
        success = tweet.unlike(self.bob)
        self.assertTrue(success)
        self.assertEqual(tweet.like_count, 0)

if __name__ == '__main__':
    unittest.main()

Debugging Spatial Applications#

Debugging spatial applications requires understanding graph state and walker movement patterns.

Debug Output and Tracing#

Debug Walker for Graph Inspection

# debug_walker.jac
walker debug_graph {
    has visited_nodes: list[str] = [];
    has visited_edges: list[str] = [];
    has max_depth: int = 3;
    has current_depth: int = 0;

    can debug_node with Profile entry {
        if self.current_depth >= self.max_depth {
            print(f"Max depth {self.max_depth} reached at {here.username}");
            return;
        }

        node_info = f"Profile: {here.username} (followers: {here.follower_count})";
        self.visited_nodes.append(node_info);
        print(f"Depth {self.current_depth}: {node_info}");

        # Debug outgoing relationships
        following = [->:Follow:->];
        tweets = [->:Post:->];

        print(f"  Following: {len(following)} users");
        print(f"  Posted: {len(tweets)} tweets");

        # Visit connected nodes
        self.current_depth += 1;
        visit following;
        visit tweets;
        self.current_depth -= 1;
    }

    can debug_tweet with Tweet entry {
        tweet_info = f"Tweet: '{here.content[:30]}...' (likes: {here.like_count})";
        self.visited_nodes.append(tweet_info);
        print(f"Depth {self.current_depth}: {tweet_info}");

        # Debug tweet relationships
        likes = [<-:Like:<-];
        comments = [->:CommentOn:->];

        print(f"  Liked by: {len(likes)} users");
        print(f"  Comments: {len(comments)}");
    }

    can debug_comment with Comment entry {
        comment_info = f"Comment: '{here.content[:20]}...'";
        self.visited_nodes.append(comment_info);
        print(f"Depth {self.current_depth}: {comment_info}");
    }
}

# Usage in tests
test debug_graph_structure {
    # Setup complex graph
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");
    root spawn create_tweet(content="Alice's first tweet");

    bob = Profile(username="bob");
    bob spawn follow_request();

    # Debug the graph
    debugger = debug_graph(max_depth=2);
    root spawn debugger;

    print("=== Debug Summary ===");
    print(f"Visited {len(debugger.visited_nodes)} nodes");
    for node in debugger.visited_nodes {
        print(f"  {node}");
    }
}

Walker State Inspection#

Walker State Testing

# walker_testing.jac
walker feed_loader {
    has user_id: str;
    has loaded_tweets: list[dict] = [];
    has users_visited: set[str] = set();
    has errors: list[str] = [];

    can load_user_feed with Profile entry {
        if here.username in self.users_visited {
            self.errors.append(f"Duplicate visit to {here.username}");
            return;
        }

        self.users_visited.add(here.username);

        # Load user's tweets
        user_tweets = [->:Post:-> Tweet];
        for tweet in user_tweets {
            tweet_data = {
                "author": here.username,
                "content": tweet.content,
                "likes": tweet.like_count,
                "created_at": tweet.created_at
            };
            self.loaded_tweets.append(tweet_data);
        }

        # Visit followed users
        following = [->:Follow:-> Profile];
        visit following;
    }
}

test walker_state_management {
    # Setup test data
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");
    root spawn create_tweet(content="Alice tweet 1");
    root spawn create_tweet(content="Alice tweet 2");

    bob = Profile(username="bob");
    bob spawn follow_request();
    bob spawn create_tweet(content="Bob's tweet");

    # Test walker state
    loader = feed_loader(user_id="alice");
    root spawn loader;

    # Verify walker state
    check len(loader.loaded_tweets) >= 2;  # At least Alice's tweets
    check "alice" in loader.users_visited;
    check "bob" in loader.users_visited;
    check len(loader.errors) == 0;

    # Verify tweet data structure
    alice_tweets = [t for t in loader.loaded_tweets if t["author"] == "alice"];
    check len(alice_tweets) == 2;

    for tweet in alice_tweets {
        check "content" in tweet;
        check "likes" in tweet;
        check "created_at" in tweet;
    }
}

Performance Testing and Optimization#

Performance testing ensures your spatial applications scale effectively.

Benchmark Testing#

Performance Benchmarks

# performance_tests.jac
import time;

test large_graph_performance {
    start_time = time.time();

    # Create large social network
    root spawn visit_profile();
    root spawn update_profile(new_username="central_user");

    # Create many users and connections
    num_users = 100;
    users = [];

    for i in range(num_users) {
        user = Profile(username=f"user_{i}");
        users.append(user);

        # Every 10th user follows central user
        if i % 10 == 0 {
            user spawn follow_request();
        }
    }

    creation_time = time.time() - start_time;
    print(f"Created {num_users} users in {creation_time:.2f} seconds");

    # Test graph traversal performance
    start_time = time.time();

    # Count all followers
    central_user = [root --> Profile][0];
    followers = [central_user <-:Follow:<- Profile];

    traversal_time = time.time() - start_time;
    print(f"Traversed {len(followers)} followers in {traversal_time:.4f} seconds");

    # Performance assertions
    check creation_time < 5.0;  # Should create 100 users in under 5 seconds
    check traversal_time < 0.1;  # Should traverse quickly
    check len(followers) == 10;  # Every 10th user = 10 followers
}

test memory_efficiency {
    # Test memory usage with large datasets
    initial_profiles = len([root --> Profile]);

    # Create and delete many objects
    for batch in range(5) {
        # Create batch of tweets
        for i in range(20) {
            root spawn create_tweet(content=f"Batch {batch} tweet {i}");
        }

        # Delete half of them
        tweets = [root --> Profile ->:Post:-> Tweet];
        for i in range(10) {
            if len(tweets) > i {
                tweets[i] spawn remove_tweet();
            }
        }
    }

    # Check memory cleanup
    final_tweets = [root --> Profile ->:Post:-> Tweet];
    check len(final_tweets) <= 50;  # Should not accumulate indefinitely

    final_profiles = len([root --> Profile]);
    check final_profiles == initial_profiles + 1;  # Only the test profile added
}

test concurrent_operations {
    # Simulate concurrent-like operations
    root spawn visit_profile();
    root spawn update_profile(new_username="concurrent_user");

    # Create multiple walkers that operate simultaneously
    walkers = [];
    for i in range(10) {
        walker = create_tweet(content=f"Concurrent tweet {i}");
        walkers.append(walker);
    }

    # Execute all walkers
    start_time = time.time();
    for walker in walkers {
        root spawn walker;
    }
    execution_time = time.time() - start_time;

    # Verify all operations completed
    all_tweets = [root --> Profile ->:Post:-> Tweet];
    check len(all_tweets) == 10;

    # Performance check
    check execution_time < 1.0;  # Should complete quickly

    print(f"Executed {len(walkers)} operations in {execution_time:.4f} seconds");
}

Memory and Resource Testing#

Resource Usage Tests

# resource_tests.jac
import gc;
import psutil;
import os;

test memory_usage_monitoring {
    # Get initial memory usage
    process = psutil.Process(os.getpid());
    initial_memory = process.memory_info().rss;

    # Create large graph structure
    root spawn visit_profile();
    root spawn update_profile(new_username="memory_test_user");

    # Create many interconnected objects
    for i in range(1000) {
        root spawn create_tweet(content=f"Memory test tweet {i}");
    }

    # Force garbage collection
    gc.collect();

    # Check memory after creation
    after_creation_memory = process.memory_info().rss;
    memory_increase = after_creation_memory - initial_memory;

    print(f"Memory increase: {memory_increase / 1024 / 1024:.2f} MB");

    # Clean up
    tweets = [root --> Profile ->:Post:-> Tweet];
    for tweet in tweets {
        tweet spawn remove_tweet();
    }

    # Force garbage collection again
    gc.collect();

    # Check memory after cleanup
    final_memory = process.memory_info().rss;
    memory_recovered = after_creation_memory - final_memory;

    print(f"Memory recovered: {memory_recovered / 1024 / 1024:.2f} MB");

    # Memory should not grow indefinitely
    check memory_increase < 100 * 1024 * 1024;  # Less than 100MB increase
    check memory_recovered > memory_increase * 0.5;  # At least 50% recovered
}

Test-Driven Development with OSP#

TDD works naturally with Jac's spatial programming model.

TDD Example: Building a Recommendation System#

TDD Recommendation System

# recommendation_system.test.jac

# Test 1: Basic recommendation structure
test recommendation_system_structure {
    # Red: This will fail initially
    root spawn visit_profile();
    root spawn update_profile(new_username="test_user");

    # Should be able to get recommendations
    recommendations = root spawn get_recommendations(limit=5);
    check isinstance(recommendations, list);
    check len(recommendations) <= 5;
}

# Test 2: Friend-based recommendations
test friend_based_recommendations {
    # Setup: Create network
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");

    bob = Profile(username="bob");
    charlie = Profile(username="charlie");

    # Alice follows Bob, Bob follows Charlie
    bob spawn follow_request();
    # Switch context to Bob and follow Charlie
    # (In real system, would handle user context switching)

    # Alice should get Charlie recommended (friend of friend)
    recommendations = root spawn get_recommendations(limit=5);
    recommended_usernames = [rec["username"] for rec in recommendations];

    # Charlie should be recommended as friend of friend
    check "charlie" in recommended_usernames;
}

# Test 3: Interest-based recommendations
test interest_based_recommendations {
    # Setup users with similar interests (tweets)
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");
    root spawn create_tweet(content="I love programming");

    bob = Profile(username="bob");
    bob spawn create_tweet(content="Programming is awesome");

    charlie = Profile(username="charlie");
    charlie spawn create_tweet(content="I hate programming");

    # Get recommendations
    recommendations = root spawn get_recommendations(
        limit=5,
        algorithm="interest_based"
    );

    # Bob should rank higher than Charlie due to similar interests
    bob_score = 0;
    charlie_score = 0;

    for rec in recommendations {
        if rec["username"] == "bob" {
            bob_score = rec["score"];
        }
        if rec["username"] == "charlie" {
            charlie_score = rec["score"];
        }
    }

    check bob_score > charlie_score;
}

# Test 4: Recommendation filtering
test recommendation_filtering {
    # Setup
    root spawn visit_profile();
    root spawn update_profile(new_username="alice");

    # Create users alice already follows
    bob = Profile(username="bob");
    bob spawn follow_request();

    # Create users alice doesn't follow
    charlie = Profile(username="charlie");
    diana = Profile(username="diana");

    # Get recommendations
    recommendations = root spawn get_recommendations(limit=10);
    recommended_usernames = [rec["username"] for rec in recommendations];

    # Should not recommend users already followed
    check "bob" not in recommended_usernames;

    # Should recommend unfollowed users
    check "charlie" in recommended_usernames or "diana" in recommended_usernames;
}

Now implement the actual recommendation system to make tests pass:

# recommendation_system.jac
walker get_recommendations(visit_profile) {
    has limit: int = 5;
    has algorithm: str = "hybrid";
    has recommendations: list[dict] = [];

    can generate_recommendations with Profile entry {
        current_user = here;
        followed_users = [->:Follow:-> Profile];
        followed_usernames = set([user.username for user in followed_users]);

        # Get all users except current and already followed
        all_users = [root --> Profile](?username != current_user.username);
        candidate_users = [user for user in all_users
                         if user.username not in followed_usernames];

        # Score each candidate
        for candidate in candidate_users {
            score = self.calculate_recommendation_score(
                current_user, candidate, followed_users
            );

            if score > 0 {
                self.recommendations.append({
                    "username": candidate.username,
                    "score": score,
                    "reason": self.get_recommendation_reason(
                        current_user, candidate, followed_users
                    )
                });
            }
        }

        # Sort by score and limit results
        self.recommendations.sort(key=lambda x: x["score"], reverse=True);
        self.recommendations = self.recommendations[:self.limit];

        report self.recommendations;
    }

    def calculate_recommendation_score(
        current_user: Profile,
        candidate: Profile,
        followed_users: list[Profile]
    ) -> float {
        score = 0.0;

        if self.algorithm in ["friend_based", "hybrid"] {
            # Friend of friend scoring
            candidate_followers = [candidate <-:Follow:<- Profile];
            mutual_connections = set([u.username for u in followed_users]) &
                               set([u.username for u in candidate_followers]);
            score += len(mutual_connections) * 2.0;
        }

        if self.algorithm in ["interest_based", "hybrid"] {
            # Interest-based scoring using tweet content
            current_tweets = [current_user ->:Post:-> Tweet];
            candidate_tweets = [candidate ->:Post:-> Tweet];

            # Simple keyword matching (in real system, use embeddings)
            current_words = set();
            for tweet in current_tweets {
                current_words.update(tweet.content.lower().split());
            }

            candidate_words = set();
            for tweet in candidate_tweets {
                candidate_words.update(tweet.content.lower().split());
            }

            common_words = current_words & candidate_words;
            score += len(common_words) * 0.5;
        }

        return score;
    }

    def get_recommendation_reason(
        current_user: Profile,
        candidate: Profile,
        followed_users: list[Profile]
    ) -> str {
        # Determine primary reason for recommendation
        candidate_followers = [candidate <-:Follow:<- Profile];
        mutual_connections = set([u.username for u in followed_users]) &
                           set([u.username for u in candidate_followers]);

        if mutual_connections {
            return f"Friends with {list(mutual_connections)[0]}";
        }

        return "Similar interests";
    }
}

Best Practices#

Testing Best Practices

  • Write tests first: Use test-driven development for complex walker logic
  • Test graph structures: Verify node and edge relationships are correct
  • Use descriptive names: Make test intentions clear from the test name
  • Test edge cases: Include boundary conditions and error scenarios
  • Isolate test data: Ensure tests don't interfere with each other
  • Mock external dependencies: Test walker logic independently of external services

Key Takeaways#

What We've Learned

Testing Framework:

  • Built-in testing: Native test blocks eliminate external framework dependencies
  • Graph testing: Specialized patterns for testing spatial relationships
  • Walker testing: Comprehensive testing of mobile computation patterns
  • Type-safe assertions: Leverage Jac's type system in test validation

Debugging Techniques:

  • Debug output: Strategic print statements and debug flags
  • Walker tracing: Track walker movement through graph structures
  • State inspection: Examine node and edge states during execution
  • Error handling: Graceful handling of edge cases and failures

Test Organization:

  • Modular testing: Organize tests by functionality and complexity
  • Helper functions: Reusable setup code for consistent test environments
  • Performance testing: Monitor execution time and resource usage
  • Integration testing: Test interactions between multiple walkers

Quality Assurance:

  • Comprehensive coverage: Test all code paths and error conditions
  • Regression prevention: Automated tests prevent breaking changes
  • Documentation value: Tests serve as executable specifications
  • Continuous validation: Automated testing in CI/CD pipelines

Try It Yourself

Enhance your testing skills by: - Writing comprehensive test suites for existing walker logic - Implementing performance benchmarks for graph operations - Creating integration tests for multi-walker scenarios - Adding debug instrumentation to complex graph traversals

Remember: Good tests make development faster and more reliable!


Ready to learn about deployment strategies? Continue to Chapter 19: Deployment Strategies!