1

I'm having trouble figuring out how to unit test a function. It's used for getting user input password-style, so that asterisks appear instead of what the user has typed. So I'm trying to capture console I/O to compare it to the expected values.

This is the function:

public string getMaskedInput(string prompt)
{
    string pwd = "";
    ConsoleKeyInfo key;
    do
    {
        key = Console.ReadKey(true);
        if (key.Key != ConsoleKey.Backspace && key.Key != ConsoleKey.Enter)
        {
            pwd = pwd += key.KeyChar;
            Console.Write("*");
        }
        else
        {
            if (key.Key == ConsoleKey.Backspace && pwd.Length > 0)
            {
                pwd = pwd.Substring(0, pwd.Length - 1);
                Console.Write("\b \b");
            }
        }

    }
    while (key.Key != ConsoleKey.Enter);
    return pwd;
}

And the test:

public void getInputTest()
{
    //arrange
    var sr = new StringReader("a secret");
    var sw = new StringWriter();
    Console.SetOut(sw);
    Console.SetIn(sr);
    Input i = new Input();
    string prompt="what are you typing? ";

    //act
    string result = i.getMaskedInput(prompt);         

    //assert
    var writeResult = sw.ToString();
    Assert.IsTrue((writeResult == "what are you typing? ")&&(result=="a secret"));

EDIT: I rechecked my unit test and it had a bug in it; now that I've fixed it, the test just hangs. Stepping through the test indicates that it has something to do with Console.ReadKey(), which I suspect can't be redirected with StreamReader() the way ReadLine() can.

Also, this seems to be two asserts in the same test, is that the right way to test this function?

Nkosi
  • 235,767
  • 35
  • 427
  • 472
sigil
  • 9,370
  • 40
  • 119
  • 199
  • The code looks similar to the stackoverflow posting http://stackoverflow.com/questions/3404421/password-masking-console-application – MethodMan Dec 20 '12 at 00:23
  • @djkraze Yes, I looked at that answer when I was developing the function. – sigil Dec 20 '12 at 00:35
  • Yep two asserts is a "bad practice" unit test should be only related to one thing. But sometimes it is acceptable. – Michal Franc Dec 20 '12 at 19:08

2 Answers2

8

You shouldn't be unit testing that kind of behaviour. This code depends heavily on external data from console.

But if you are forced to test this ...

First of all break the dependancy on console. Wrap Console operations like Console.Read and Console.Write with some class.

public class ConsoleWrapper : IConsoleWrapper
{
    public ConsoleKeyInfo ReadKey()
    {
        return Console.ReadKey(true);
    }

    public void Write(string data)
    {
        Console.Write(data);
    }
}

Also there is an interface IConsoleWrapper

public interface IConsoleWrapper
{
    ConsoleKeyInfo ReadKey();
    void Write(string data);
}

Now in your function you can do

    public static string GetMaskedInput(string prompt, IConsoleWrapper console)
    {
        string pwd = "";
        ConsoleKeyInfo key;
        do
        {
            key = console.ReadKey();
            if (key.Key != ConsoleKey.Backspace && key.Key != ConsoleKey.Enter)
            {
                pwd += key.KeyChar;
                console.Write("*");
            }
            else
            {
                if (key.Key == ConsoleKey.Backspace && pwd.Length > 0)
                {
                    pwd = pwd.Substring(0, pwd.Length - 1);
                    console.Write("\b \b");
                }
            }
        }
        while (key.Key != ConsoleKey.Enter);
        return pwd;
    }
}

With this interface you can now mock it and easily check called methods and its parameters. Also you can create some ConsoleStub with internal string that can simulate whole operation.

Something like this.

public class ConsoleWrapperStub : IConsoleWrapper
{
    private IList<ConsoleKey> keyCollection;
    private int keyIndex = 0;

    public ConsoleWrapperStub(IList<ConsoleKey> keyCollection)
    {
        this.keyCollection = keyCollection;
    }

    public string Output = string.Empty;

    public ConsoleKeyInfo ReadKey()
    {
        var result = keyCollection[this.keyIndex];
        keyIndex++;
        return new ConsoleKeyInfo( (char)result ,result ,false ,false ,false);
    }

    public void Write(string data)
    {
        Output += data;
    }
}

This stub gives you ability to create your own contained test scenarios.

For instance

    [Test]
    public void If_Enter_first_then_return_empty_pwd()
    {
        // Arrange
        var stub = new ConsoleWrapperStub(new List<ConsoleKey> { ConsoleKey.Enter });
        var expectedResult = String.Empty;
        var expectedConsoleOutput = String.Empty;

        // Act

        var actualResult = Program.GetMaskedInput(string.Empty, stub);

        //     
        Assert.That(actualResult, Is.EqualTo(expectedResult));
        Assert.That(stub.Output, Is.EqualTo(expectedConsoleOutput));
    }

    [Test]
    public void If_two_chars_return_pass_and_output_coded_pass()
    {
        // Arrange
        var stub = new ConsoleWrapperStub(new List<ConsoleKey> { ConsoleKey.A, ConsoleKey.B, ConsoleKey.Enter });
        var expectedResult = "AB";
        var expectedConsoleOutput = "**";

        // Act

        var actualResult = Program.GetMaskedInput(string.Empty, stub);

        //     
        Assert.That(actualResult, Is.EqualTo(expectedResult));
        Assert.That(stub.Output, Is.EqualTo(expectedConsoleOutput));
    }

Hopefully this helped you and you get the general idea :)

EDIT

Ok i have edited my samples and tested them with Nunit it works. But you have to remember that each test scenario has to end with ENTER key. Without it the while loop will be endless and there will be exception KeynotFound beacuse we have limited set of characters in the List.

Michal Franc
  • 1,036
  • 10
  • 16
  • In the IConsoleWrapper interface, I got the error `The modifier 'public' is not valid for this item.` so I removed `public`. I'm trying to use your test example but `ConsoleKey.Enter` is a `ConsoleKey`, not a `ConsoleKeyInfo`, so i'm not able to create `var stub`. I tried creating an instance of `ConsoleKeyInfo` with the character 'x' and using it to populate `List`, adjusting `expectedResult` accordingly, but that threw an exception. I'm wondering if it's worth the trouble to unit test this, since it doesn't depend on any data structures that might change. – sigil Dec 20 '12 at 18:47
  • 1
    Ok i was just writing this example without visual studio ;P on a paper :D What kind of exception ? – Michal Franc Dec 20 '12 at 18:58
  • Changing the IList to type eliminated the exception. Thanks for helping with this! – sigil Dec 20 '12 at 21:23
0

I made a few tweaks in case anyone is interested. It handles mixed case and other console output, such as prompts and new line characters.

PromptForPassword:

    internal static SecureString PromptForPassword(IConsoleWrapper console)
    {
        console.WriteLine("Enter password: ");
        var pwd = new SecureString();
        while (true)
        {
            ConsoleKeyInfo i = console.ReadKey(true);
            if (i.Key == ConsoleKey.Enter)
            {
                break;
            }
            else if (i.Key == ConsoleKey.Backspace)
            {
                if (pwd.Length > 0)
                {
                    pwd.RemoveAt(pwd.Length - 1);
                    console.Write("\b \b");
                }
            }
            else if (i.KeyChar != '\u0000') // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc
            {
                pwd.AppendChar(i.KeyChar);
                console.Write("*");
            }
        }
        console.WriteLine();
        return pwd;
    }

Interface:

public interface IConsoleWrapper
{
    void WriteLine(string value);
    ConsoleKeyInfo ReadKey(bool intercept);
    void Write(string value);
    void WriteLine();
    string ReadLine();
}

MockConsoleStub:

public class MockConsoleStub : IConsoleWrapper
{
    private readonly IList<ConsoleKeyInfo> ckiCollection;
    private int keyIndex = 0;

    public MockConsoleStub(IList<ConsoleKeyInfo> mockKeyInfoCollection)
    {
        ckiCollection = mockKeyInfoCollection;
    }

    public readonly StringBuilder Output = new StringBuilder();

    public ConsoleKeyInfo ReadKey()
    {
        var cki = ckiCollection[this.keyIndex];
        keyIndex++;
        return cki;
    }

    public void Write(string data)
    {
        Output.Append(data);
    }

    public void WriteLine(string value)
    {
        Output.AppendLine(value);
    }

    public void WriteLine()
    {
        Output.AppendLine();
    }

    public ConsoleKeyInfo ReadKey(bool intercept)
    {
        var cki = ckiCollection[this.keyIndex];
        keyIndex++;
        return cki;
    }

    public string ReadLine()
    {
        var sb = new StringBuilder();
        var cki = ckiCollection[this.keyIndex];
        keyIndex++;
        while (cki.Key != ConsoleKey.Enter)
        {
            sb.Append(cki.KeyChar);
            cki = ckiCollection[keyIndex];
            keyIndex++;
        }
        return sb.ToString();
    }
}

Usage:

    [TestMethod]
    public void PromptForUsername_stub_password_GetsPassword()
    {
        var stub = new MockConsoleStub(new List<ConsoleKeyInfo>
        {
            new ConsoleKeyInfo('P', ConsoleKey.P, true, false, false),
            new ConsoleKeyInfo('@', ConsoleKey.Attention, true, false, false),
            new ConsoleKeyInfo('s', ConsoleKey.S, false, false, false),
            new ConsoleKeyInfo('s', ConsoleKey.S, false, false, false),
            new ConsoleKeyInfo('w', ConsoleKey.W, false, false, false),
            new ConsoleKeyInfo('o', ConsoleKey.O, false, false, false),
            new ConsoleKeyInfo('r', ConsoleKey.R, false, false, false),
            new ConsoleKeyInfo('d', ConsoleKey.D, false, false, false),
            new ConsoleKeyInfo('!', ConsoleKey.D1, true, false, false),
            new ConsoleKeyInfo('\u0000', ConsoleKey.Enter, false, false, false),
        });
        var password = Settings.PromptForPassword(stub);
        Assert.AreEqual("P@ssword!", SecureStringWrapper.ToString(password));
        Assert.AreEqual($"Enter password: {Environment.NewLine}*********{Environment.NewLine}", stub.Output.ToString());
    }

Note: SecureStringWrapper returns either a Byte array or a string. For testing, I return a string.

Mark Good
  • 4,271
  • 2
  • 31
  • 43