I tried out Test Driven Development.

By akash_rawal, created: 2023-03-27, last modified: 2023-11-18

I decided to switch from writing unit tests after writing implementation to using Test Driven Development. In the process, I document my first experiences.

Introduction

I apologize for the under-construction vibe that this site gives off. I am building it incrementally, and even I don't know how it will end up looking.

Recently, while writing code for this site, I decided to switch from writing unit tests after writing implementation to using Test Driven Development. So now I have unit tests that were written afterwards and unit tests that were written before implementation. And the tests look very different.

First differences

Here are the tests for a module I wrote earlier without TDD.

    #[tokio::test]
    async fn test() {
        drop(env_logger::try_init());

        log::info!("Creating test database");
        let pool = SERVER.connect().await;
        let mut mgr = mgt::Manager::new(); 
        mgr.add(mod_info());
        mgr.add(users::mod_info());
        mgr.migrate(&pool).await
            .expect("Migration failed");

        log::info!("Creating user user1");
        let user1 = users::create(&pool, "user1").await
            .expect("Unable to create user");

        log::info!("Creating new session");
        let session = create(&pool, &user1.uid, 3).await
            .expect("Unable to create new session");
        assert_eq!(session.uid, user1.uid);

        log::info!("Checking for active session");
        let chk_value = get(&pool, &session.sessionid).await
            .expect("Unable to get session via sessionid");
        assert_eq!(chk_value, session);

        log::info!("Testing session deletion");
        delete(&pool, &session.sessionid).await
            .expect("Unable to delete session");
        let chk_value = get(&pool, &session.sessionid).await;
        assert!(matches!(chk_value, Err(Error::RowNotFound)));

        log::info!("Testing foreign key");
        users::delete(&pool, &user1.uid).await
            .expect("Unable to delete user");
        let chk_value = create(&pool, &user1.uid, 3).await;
        assert!(matches!(chk_value, Err(Error::Database(_))));

        log::info!("Testing session pruning");
        let user1 = users::create(&pool, "user1").await
            .expect("Unable to create user");
        for _ in 0..5 {
            create(&pool, &user1.uid, 3).await
                .expect("Unable to create new session");
        }
        log::info!("Assertions for session pruning");
        let chk_value = list(&pool).await.unwrap();
        assert_eq!(chk_value.len(), 4);
    }

It is hard to figure out in a glance what the test is checking for. It is also not obvious what the module does from just the tests. The code is written in Rust, and if you did not know Rust, the logging statements might be the only thing that tells you what the test is doing.

Here are the tests for the first module I wrote with TDD.

    #[tokio::test]
    async fn test_get() {
        let (pool, user1, _) = init().await;
        let form = test_blog();

        let id = create(&pool, &user1.uid, &form).await
            .expect("create");
        let blog = get(&pool, id, Access::All).await
            .expect("get");
        assert_eq!(&blog.title, &form.title);
        assert_eq!(&blog.body, &form.body);
        assert_eq!(&blog.author_name, &user1.username);
    }

    #[tokio::test]
    async fn test_get_others() {
        let (pool, user1, user2) = init().await;
        let form = test_blog();

        let id = create(&pool, &user1.uid, &form).await
            .expect("create");
        let result = get(&pool, id, Access::Mine(user2.uid.clone())).await;
        assert!(matches!(result, Err(sqlx::Error::RowNotFound)));
    }

    #[tokio::test]
    async fn test_modify() {
        let (pool, user1, _) = init().await;
        let mut form = test_blog();

        let id = create(&pool, &user1.uid, &form).await
            .expect("create");

        form.body = "Modified body".into();
        modify(&pool, id, &form, Access::All).await
            .expect("modify");
        let blog = get(&pool, 1, Access::All).await
            .expect("get");
        assert_eq!(&blog.body, &form.body);
    }

    #[tokio::test]
    async fn test_modify_others() {
        let (pool, user1, user2) = init().await;
        let mut form = test_blog();

        let id = create(&pool, &user1.uid, &form).await
            .expect("create");

        form.body = "Modified body".into();
        let access = Access::Mine(user2.uid.clone());
        let result = modify(&pool, id, &form, access).await;
        assert!(matches!(result, Err(sqlx::Error::RowNotFound)));
    }

    #[tokio::test]
    async fn test_index() {
        let (pool, user1, _) = init().await;
        let form = test_blog();

        create(&pool, &user1.uid, &form).await
            .expect("create");

        let idx = index(&pool, Access::All).await
            .expect("index");
        assert_eq!(idx.len(), 1);
        let idx = index(&pool, Access::Mine(user1.uid.clone())).await
            .expect("index");
        assert_eq!(idx.len(), 1);
    }

    #[tokio::test]
    async fn test_index_others() {
        let (pool, user1, user2) = init().await;
        let form = test_blog();

        create(&pool, &user1.uid, &form).await
            .expect("create");

        let idx = index(&pool, Access::Mine(user2.uid.clone())).await
            .expect("index");
        assert_eq!(idx.len(), 0);
    }

It is much more obvious that the module handles displaying and editing blog posts on this site. It is also obvious that it cannot delete blog posts yet.

However, if I look at the names of the tests, I see a cartesian product.

Who create modify index
Self test_get test_modify test_index
Others test_get_others test_modify_others test_index_others

I do not like it. To me it looks like there is redundancy involved, and a change in the implementation might require multiple tests to be updated.

It is possible that this is actually good code and I haven't realized the merits yet. It is also possible that this is actually bad code and I did TDD wrong.

More complex modules

So far I do not have experience in using TDD for more complex code. In the past, I have written tests that involve loops or tree-generation algorithms to check each possible case. I wonder how such tests will work with TDD involved. But then I also lack any experience in how to grade tests written with TDD. I guess only time and experience will tell.