Click here to Skip to main content
Click here to Skip to main content

Tagged as

Unit test and the man in the middle

, 22 May 2012
Rate this:
Please Sign up or sign in to vote.
How to unit test network resources access : The hacker way.
(xkcd.com)

Unit test and the man in the middle

The pain

I hate slow tests... You know, the kind of test that cause a coffee addiction... Not because you like coffee, you are just bored to death by waiting.

Some among you will preach the benefit of mock testing. That's great, but mock testing work in test, and this stupid MockMailSender will not work so well in real life.

Above all, I don't like writing code, it's buggy, and there is always someone to put you down on each line of code.
"What's your solution Nicolas ?" I can read in your curious eyes.

The cure : A real proxy man-in-the-middle

Well, just run the test one time, with a man-in-the-middle (proxy) between your networks ressource and your test, record messages in a local folder. And next time you run the test, just ask man-in-the-middle to send again the same sequence of byte.
Picture maestro !

Step 1 : You record messages between your machine and the server.

Step 2 : You replay messages from your local store. (By default, a folder)

How cool is that ?

I used it in my own code to test code that interact with SMTP/IMAP server, so how it works in real code ?

static void Main(string[] args)
{
    string dataFromCP;
    string dataFromProxy;

    ProxyRecorder recorder = new ProxyRecorder(8172, "www.codeproject.com", 80);

    using(recorder.Record("CPData"))
    {
        WebClient client = new WebClient();
        dataFromCP = client.DownloadString("http://localhost:8172/");
    }

    //You can cut the network connection, reboot your computer, or work on the beach here !
    using(recorder.Replay("CPData"))
    {
        WebClient client = new WebClient();
        dataFromProxy = client.DownloadString("http://localhost:8172/");
    }
    AssertAreEqual(dataFromCP, dataFromProxy);
}

However, there are some problems to be aware of with this approach...

Not the silver bullet yet

The problem is that some protocol are replay replay proof. In other words, the client will detect that these messages are not the real one.
That's the case with SSL, but don't be sad so fast, I've not told your the end of the story.

My code works with SMTP and IMAP over SSL.

To do that, I create a fake certificate with my proxy, and tell my client to trust it.
My proxy then, can decrypt and encrypt client data, and negotiate its own SSL session with the real server to reencrypt it.

ProxyRecorder recorder = new ProxyRecorder(8172, "smtp.codeproject.com", 593);
recorder.UseSmtpOverSSL = true;
ServicePointManager.ServerCertificateValidationCallback += (a, b, c, d) => true; //Accept invalid certificate
using(recorder.Record("CPData"))
{
    SmtpClient client = new SmtpClient();
    client.Host = "localhost";
    client.Port = 8172;
    client.EnableSsl = true;
    //...do your stuff

But wait, there is more : in the case of HTTP, I cheated on my previous example...

Imagine you want to go on a website like www.google.fr, it will try to redirect you to www.google.com... And so, the WebClient will then send a request to google and not to your proxy !

No problem I thought about it (thanks to this article) you just have to specify that you are using a proxy to WebClient :

static void Main(string[] args)
{
    string dataFromCP;
    string dataFromProxy;

    ProxyRecorder recorder = new ProxyRecorder(8172, "www.codeproject.com", 593);
    using(recorder.Record("CPData"))
    {
        WebClient client = new WebClient();
        client.Proxy = recorder.CreateHttpWebProxy();
        dataFromCP = client.DownloadString("http://www.codeproject.com");
    }

    //You can cut the network connection here !
    using(recorder.Replay("CPData"))
    {
        WebClient client = new WebClient();
        client.Proxy = recorder.CreateHttpWebProxy();
        dataFromProxy = client.DownloadString("http://www.codeproject.com");
    }
    AssertAreEqual(dataFromCP, dataFromProxy);
}

All HTTP requests will endup with the right domain, and will pass through your proxy.

For WCF and SOAP message with WS-Security, you will need to deactivate detect replay attack, and set the ClockMaxSkew to maximum value on your client.

Show me the code

That's very simple, you only have one extension point to change the message persistence store.

By default all messages are stored in a folder a zip file. And I would appreciate if someone could develop the IMessageRepository to store in a zip file instead... (Done since 22 may)

The actual implementation is very simple, it just open two socket : one for the client, and one for the server, then copy the data from one to another.

Here is the code of the Recorder: (parent.Protocol.CreateWrapperStream will wrap the network stream depending on the ProxyRecorder.Protocol used. It can be Http, Smtp, default -automatic detection with port number- or simple -protocol with no SSL bootstrapping like IMAP-)

void ClientAccepted(IAsyncResult ar)
{
    var clientStream = parent.Protocol.CreateWrapperStream(new NetworkStream(socket.EndAccept(ar)), false);
    var targetStream = parent.Protocol.CreateWrapperStream(new NetworkStream(parent.ConnectToTarget()), true);
    Route(clientStream, targetStream, false);
    Route(targetStream, clientStream, true);
}

object _lock = new object();

private void Route(Stream source, Stream target, bool sourceIsTarget)
{
    var buffer = new byte[10000];
    Route(source, target, buffer, sourceIsTarget);
}
private void Route(Stream source, Stream target, byte[] buffer, bool fromTarget)
{
    ReThrowIfNotDisposed(() =>
    {
        source.BeginRead(buffer, 0, buffer.Length, ar =>
        {
            ReThrowIfNotDisposed(() =>
            {
                var count = source.EndRead(ar);
                lock(_lock)
                {
                    target.Write(buffer, 0, count);
                    _Stream = _MessageStream.CreateNextMessage(fromTarget);
                    _Stream.Write(buffer, 0, count);
                }
                Route(source, target, buffer, fromTarget);
            });
        }, null);
    });
}

For the Replay part, I just iterate over all messages from the store and dispose them or send it to client.

private void Listen(Stream clientStream, byte[] buffer)
{
    ReThrowIfNotDisposed(() =>
    {
        var message = _MessageStream.GetNextMessage();
        if(message != null)
        {
            if(message.IsTarget)
            {
                clientStream.Write(message.Content, 0, message.Content.Length);
                Listen(clientStream, buffer);
            }
            else
            {
                clientStream.Read(buffer, 0, buffer.Length);
                Listen(clientStream, buffer);
            }
        }
    });
}

More to come

I did not test yet on database connection, but I think it should work, since there is no credential negotiation with anti replay by default if I remember.

Anyway, thanks for reading, I hope it will change your life, and that you will live longer. If some protocols are not supported, just tell me.

And last thing : I want to thank code project for changing the article submission wizard, that was a moment of joy for me :')

History

22 May 2012 : Lots of bug fix, sockets are properly disposed on recorder and replayer dispose. Support saving traffic in .zip by default instead of folder. Support HTTPS.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nicolas Dorier
Software Developer Freelance
France France
I am a trainer and a curious developer.
 
CEO of AO-IS, we created a tool to make IaaS on Azure more easy IaaS Management Studio.
 
If you are interested for working with me, for fun coding stuff, for freelance stuff, or interested in using our cloud training infrastructure freely for a kickass presentation for the dev community ? this way Smile | :)

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMohammad A Rahman22-May-12 17:26 
GeneralRe: My vote of 5 PinmemberNicolas Dorier23-May-12 4:33 
GeneralRe: My vote of 5 PinmemberMohammad A Rahman23-May-12 4:35 
Questionreinventing the weel... Pinmemberdothebart18-May-12 0:16 
AnswerRe: reinventing the weel... PinmemberNicolas Dorier18-May-12 3:26 
GeneralMy vote of 5 PinmemberNab6912-May-12 4:50 
GeneralClever idea, with a few potential pitfalls PinmemberJohn Brett11-May-12 2:22 
GeneralRe: Clever idea, with a few potential pitfalls PinmemberNicolas Dorier11-May-12 3:12 
I've seen cases where the unit test used a capture/replay technique to short-cut the manual generation of expectations.
The goal of this proxy is not to short-cut the manual generation of expectation. I think it's a bad idea to do that, for the same reason you specified.
For example, in my case, I wanted to be sure that some emails I could get in IMAP from Gmail would be effectively processed by my tested class and would update the database correctly.
My expectation were : Given such data recieved from Gmail, you should have added a row to the database.
The input was generated partially (I've sent an email to Gmail manually, so I could get it with IMAP when recording), but my output was not.
 
There are also some risks about variations in or specific implementations of the protocols. By snap-shotting a specific implementation, there's the risk that the code won't handle variations of the data traffic. To be fair, this is a general issue with testing against live systems.
 
Yes, that happen very often unfortunalely, but I think recording/replaying each protocol implementation can be a very effective way to unit test them all without having the network dependency and latence. I would record in one folder every interaction with specific implementations, and replay each one in unit tests.
So this way, you would have only one test but running each different implementation.
 
Finally, in any test suite that actually uses an uncontrolled resource (such as the network) there's the risk that environmental factors will conspire to generate false negatives on the test results. If the proxy goes down, then so do all of the tests.

That's a good reason to use the proxy and not the actual external resource : the test is repeatable. The proxy is not hosted in a separate process, but in the same process as your test, so you should have not any false negatives on tests results.
 
I'd still probably prefer to mock out the network layer where possible, and get as much unit/integration testing in place as possible before introducing the network. 
I agree that it can be better to just mock the network layer, especially if system integration is a small part of your application.
But, when an application is very dependant upon several systems, most of the bugs will appear during real integration and system testing. In my case, in 200 lines of code, my application were using SMTP to send mail, IMAP to recieve mail, and HTTP to crawl a website, and some database update stuff, mocking these 3 parts implied mocking 90% of my code which make the test useless.
Also, I don't really like introducing new interface, or abstract class for the only reason of testing.
More generality means also more complexity for the user of this class, which can trigger other bugs as well. (In my case, I was sure I would only use IMAP, so I did not need an interface in the case I wanted to support POP3, so I did not want to create one interface just for testing... However I would have considered it otherwise.)

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 22 May 2012
Article Copyright 2012 by Nicolas Dorier
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid