When writing testable code your first port of call is often to abstract any dependencies and make them easy to mock. This is the same for any of your codebase that talks to FTP servers. Testing the way your code behaves under real world conditions makes integration tests important regardless of abstraction though. Here’s a simple trick to test FTP code in the wild.
A recent project of mine has involved writing code that talks to FTP servers with the goal of adding additional continuous integration automation to a project. Although all of my main methods are easily abstracted and injectable, my project still needs to actually talk to FTP servers at the end of the day, and I need to test that these very methods do the right thing when they are met with different conditions; be they bad credentials, lack of read/write permissions etc.
The Challenge
Integration tests can be brittle at the best of times, so ensuring that they are repeatable and can be setup and torn down can often be almost as much of a challenge as writing your actual code itself.
An FTP server is usually a static service that is installed on a server. You might think that running one and ensuring it stays up and doesn’t get hacked just so that all your integration tests work is a necessary evil, but there is an easier way.
Run local. Run often.
I was running an FTP server on my build server just so that it was “always around” for my tests until i stumbled across an interesting project over on GitHub to do just this.
The approach I'm about to show you doesn’t need you to go to the effort of running a dedicated server at all. All you need to do is add a single executable to your unit test project and wrap your unit test in a using statement.
The FTP server executable is a single file FTP server called FTPDMIN which offers a read/write FTP server that can be fired up from the command line with a minimum feature set and only a few command line parameters to make it all tick.
By implementing IDisposable the helper class that wraps around this command line exe allows you to take advantage of the using() pattern to take care of your executable’s lifetime and have it die when your code is done testing.
Steps to make it happen
Download FTPDMIN from here.
Add the exe to the root of your test project (you can put this anywhere, but you’ll have to update the helper class below).
Now add the exe to your project (i.e “view all items” in your test project’s solution explorer, and add the exe).
Set the EXE to “Copy always” in it’s solution properties.
Add the following code to a helper class in your Test Project:
public class FtpTestServer: IDisposable { private readonly Process ftpProcess; public FtpTestServer(string rootDirectory, int port = 21, bool allowUploads = true) { var psInfo = new ProcessStartInfo { FileName = AppDomain.CurrentDomain.BaseDirectory + "\\ftpdmin.exe", Arguments = String.Format("-p {0} -ha 127.0.0.1 \"{1}\" {2}", port, rootDirectory, allowUploads ? string.Empty : "-g"), WindowStyle = ProcessWindowStyle.Hidden }; ftpProcess = Process.Start(psInfo); } public void Dispose() { if (ftpProcess.HasExited) return; ftpProcess.Kill(); ftpProcess.WaitForExit(); } }
Now you can enjoy being able to write really clean integration testing code that starts and FTP server every time you run your tests and then tear it down when your test is done.
An example integration test showing connecting to “127.0.0.1”:
[TestMethod] public void FtpCode_Upload_CanConnect() { try { // Fire up a new Ftp server instance using (new FtpTestServer(rootDirectory: "./")) { // code that talks to an FTP server on 127.0.0.1 } } catch (WebException e) { Assert.Fail("Failed to connect to our FTP server"); } }
How awesome is that?
The power of using FTPDMIN is that it can be told to deny write permissions to simulate bad user permissions as well:
[TestMethod] public void FtpCode_Upload_ThrowsWebException() { try { // Fire up a new Ftp server instance using (new FtpTestServer(rootDirectory: "./", allowUploads: false)) { // code that talks to an FTP server on 127.0.0.1 } } catch (WebException e) { Assert.Fail("Our code failed to upload a file because of invalid permissions"); } }
All in all the above has been a complete life saver when it comes to making my integration test projects portable – if a new developer joins my project, they instantly get access to my FTP test harness just by pulling down my project’s source code.