How can I test user input?

Executing the below program prompts the user to enter a word. The word that the user enters is printed by its characters.

pub fn userInput() !void {
    var buffer: [63]u8 = undefined;
    const reader = std.io.getStdIn().reader();
    const writer = std.io.getStdOut().writer();

    try writer.print("Enter a word: \n", .{});
    const word = try reader.readUntilDelimiter(&buffer, '\n');

    if (word.len > 0) {
        for (word) |letter| {
            std.debug.print("{c} ", .{letter});
        }
    } else {
        try writer.print("No input detected ...\n", .{});
    }
}

pub fn main() !void {
    try userInput();
}

const std = @import("std");

How can I write a unit test for this ?

This is something along the lines of this

If you want to test this kind of code, you separate the side effects (getting input and producing output) from computations (splitting words, counting lengths, etc…) and you test the computation with arbitrary inputs (or maybe fuzz it).

You don’t need to test whether stdin or stdout work.

If you really want to test whether the concept of a reader and writer work, then you cna take those as parameters instead of hard-coding stdin and stdout and do the same as above, but wrapping with buffers.

Conventional wisdom for writing testable code is to take dependencies as parameters instead of creating them internally inside the function, and to take them as interfaces instead of concrete implementations so that they can be swapped out for test-specific implementations.

I would start by changing the signature of the function to fn userInput(reader: std.io.AnyReader, writer: std.io.AnyWriter) !void, and then write a test that uses two std.io.FixedBufferStreams, one for the reader and another for the writer, and tests the output written to the writer using std.testing.expectEqualStrings().

Solution spoilered in case you want to think about the problem on your own.

pub fn userInput(reader: std.io.AnyReader, writer: std.io.AnyWriter) !void {
    var buffer: [63]u8 = undefined;

    try writer.print("Enter a word:\n", .{});
    const word = std.mem.trimRight(u8, try reader.readUntilDelimiter(&buffer, '\n'), "\r");

    if (word.len > 0) {
        try writer.print("{c}", .{word[0]});
        for (word[1..]) |letter| {
            try writer.print(" {c}", .{letter});
        }
        try writer.writeAll("\n");
    } else {
        try writer.print("No input detected ...\n", .{});
    }
}

pub fn main() !void {
    const reader = std.io.getStdIn().reader().any();
    const writer = std.io.getStdOut().writer().any();
    try userInput(reader, writer);
}

const std = @import("std");

test userInput {
    const input =
        \\hello
        \\
    ;
    var reader_stream = std.io.fixedBufferStream(input);
    const reader = reader_stream.reader().any();

    var output: [1024]u8 = undefined;
    var writer_stream = std.io.fixedBufferStream(&output);
    const writer = writer_stream.writer().any();

    try userInput(reader, writer);

    try std.testing.expectEqualStrings(
        \\Enter a word:
        \\h e l l o
        \\
    , writer_stream.getWritten());
}
4 Likes

Thanks for pointing me in the right direction. I’ll look on writer_stream.getWritten() as well.

Testing without stdin/stdout is generally the better way to go, but just for completeness, this thread contains a few possible approaches for testing the program in the OP unmodified:

For the build system approach, you’d want to do something like:

test_foo.setStdIn(.{ .bytes = "foo\n" });
test_foo.expectStdOutEqual("Enter a word: \n");
test_foo.expectStdErrEqual("f o o ");
4 Likes

Thank you!

1 Like