Skip to main content

Starting a maze from the end

Developing code can be a little like trying to complete a Maze. You generally know where you want to get to and you may have an idea of where to start, but the in between is all unknown. As a kid I was told to always start the Maze from the beginning and not to cheat by starting from the end because that would spoil the fun of figuring out how to get to the finish. I feel like a lot of my development time up to this point has followed those same guidelines, start from the beginning. Most of the time I still end up getting to the end product just fine, but there are also a lot of times that I miss something, I end up with bugs or other issues in code.

Starting from the end of the Maze may be cheating for a Maze, but it shouldn't be seen as cheating when developing code. In fact it should be encouraged and used exclusively. So how do you start from the end of your code? You use Test Driven Development. You begin by creating tests that will prove that your code ends with the result you want it to. Then how you get to the end isn't a problem because you know, for certain, that the end result is what you want. If I write tests that prove that the end result will be what I want out of the code, then anyone can write the actual implementation of that code, and they can do it any way they choose to, because in the end, when the tests pass, the code will get the same results.

I want to share some real code I worked on this week and look at how it changed given feedback and how using Tests first allowed me to change the code significantly without worrying about those changes negatively affecting the final product.

The goal of this code is given a string of email addresses, separated by semicolon, and with the input of an address that needs to be removed from that string, remove that address and return the remaining addresses separated by semicolon.

So I started with a simple test that proved that if the entire string matched the remove address, I would get back an empty string.

<TestMethod()>
Public Sub WhenEmailMatchesExactlyReturnEmptyString()
 Const expected As String = ""

 Dim actual As String = EmailRemoval.GenerateEmailReplacement("test@test.com", "test@test.com")

 Assert.AreEqual(expected, actual)
End Sub

Notice that I didn't start out with any of the actual code, I don't even have the class or function created yet. I also made sure to use variables for the email addresses that make it clear that they are not important to the specifics of the test. If I used a specific email address it might get confused later for being an important piece of the logic. I also named my test function to explicitly describe what is going on and what the result should be, that way if this test fails it will be immediately obvious where the problem was.

So then I added a few more tests that are important to assure that my result is what I want. Including a test to make sure that I am not removing information if the value passed in does not match anywhere in the given string.

<TestMethod()>
Public Sub WhenEmailDoesNotMatchReturnOriginalString()
 Const expected As String = "test@test.com"

 Dim actual As String = EmailRemoval.GenerateEmailReplacement("test@test.com", "doesNotMatch@test.com")

 Assert.AreEqual(expected, actual)
End Sub

<TestMethod()>
Public Sub WhenMatchingAddressIsLastOfMultipleAddressesRemoveAddressAndLeadingSemiColon()
 Const expected As String = "test@test.com"

 Dim actual As String = EmailRemoval.GenerateEmailReplacement("test@test.com;removeMe@test.com", "removeMe@test.com")

 Assert.AreEqual(expected, actual)
End Sub

<TestMethod()>
Public Sub WhenMatchingAddressIsInTheMiddleOfMultipleAddressesRemoveThatAddressAnd1SemiColon()
 Const expected As String = "test@test.com;second@test.com"

 Dim actual As String = EmailRemoval.GenerateEmailReplacement("test@test.com;removeMe@test.com;second@test.com", "removeMe@test.com")

 Assert.AreEqual(expected, actual)
End Sub

<TestMethod()>
Public Sub WhenMatchingAddressIsTheFirstOfMultipleAddressesRemoveAddressAndTrailingSemiColon()
 Const expected As String = "test@test.com"

 Dim actual As String = EmailRemoval.GenerateEmailReplacement("removeMe@test.com;test@test.com", "removeMe@test.com")

 Assert.AreEqual(expected, actual)
End Sub

So given these tests I was able to create this function:

Public Shared Function GenerateEmailReplacement(currentEmail As String,
                                                emailBeingRemoved As String) As String
  Dim newEmail As String = currentEmail.Replace(emailBeingRemoved, "").Replace(";;", ";")
  If newEmail.EndsWith(";") Then
    newEmail = newEmail.Substring(0, newEmail.Length - 1)
  End If
  If newEmail.StartsWith(";") Then
    newEmail = newEmail.Substring(1)
  End If

  Return newEmail.Trim()
End Function
I wasn't thrilled with the if statements to remove out the semicolons on the ends of the string, but all of my tests pass so this was good enough to go on to code review.

What about an input that is a substring of an address, but does not match completely? The code reviewer noticed an issue that I had not originally considered and pointed out a potential false positive in the existing logic which could mean that this code would remove more than was intended.

So, I again start with a test, this time to verify if the new case in fact fails or passes.

<TestMethod()>
Public Sub WhenMatchingAddressBelongsToAPartOfAValidAddressTheValidAddressIsNotRemoved()
 Const expected As String = "ttest@test.com"

 Dim actual As String = EmailRemoval.GenerateEmailReplacement("ttest@test.com", "test@test.com")

 Assert.AreEqual(expected, actual)
End Sub

And it does indeed fail. And this is the beautiful part of having created those tests already, I can change my function in any way I see fit, and as long as the tests still pass when I am done, I know that it still works for all of those scenarios. Now I go about making changes to fit the new tests and eventually come to this result.

Public Shared Function GenerateEmailReplacement(currentEmail As String,
                                                emailBeingRemoved As String) As String
  Dim splitAddresses As List(Of String) = GetListOfAddresses(currentEmail)
  splitAddresses.RemoveAll(Function(address As String) AddressesMatch(address, emailBeingRemoved))

  Return String.Join(";", splitAddresses)
End Function

Public Shared Function GetListOfAddresses(ByVal emailAddresses As String) As List(Of String)
  Return emailAddresses.Split(";"c).ToList()
End Function

Public Shared Function AddressesMatch(toBeCompared As String,
                                      toBeRemoved As String) As Boolean
  Return (toBeCompared.Trim().ToUpper() = toBeRemoved.Trim().ToUpper())
End Function

It is vastly different than my original function, its separated out more so parts of are re-useable, and now all of my tests pass again.

I have included these tests and class up on github in case anyone wants to play around with this code: github.

Comments

Popular posts from this blog

Converting a Large AngularJS Application to TypeScript Part 2

In part 1 I was able to take an Angular controller written in JavaScript and convert it to a TypeScript file while doing very little to change the code. In this post I am going to explore transitioning that same controller to actually use the features provided in TypeScript. This is how I left off my controller:
declare var angular: any; (function () { 'use strict'; var controller: any = function($scope){ ... } angular .module('app') .controller('controller', controller); controller.$inject = ["$scope"]; })();
While performing the translation from JavaScript to TypeScript, I would make sure at every step that the functionality I expected still worked, so if anything I did broke the system I would change it back and try again with another approach. Also if something seemed like it worked too easily, I would break it on purpose to make sure I wasn't getting a false result through browser caching a previously working fil…

Converting a Large AngularJS Application to TypeScript Part 1

I work on a project that uses AngularJS heavily. Recently we wondered if using a preprocesser like CoffeeScript or TypeScript for our JavaScript would be beneficial. If our team is going to switch languages, we would need to be able to convert existing code over without much pain and we would have to find enough value in switching that it would be worth the conversion.

I had read an article that stated that because TypeScript is a SuperSet of JavaScript, you could convert a plain JavaScript file to TypeScript by changing the extension to .ts and not much else would need to change. I wanted to test out this claim, so I took a file that I was familiar with, an Angular Controller, and tried to convert it to TypeScript to see how much effort it would take and then try to figure out where we would benefit from using TypeScript.

This is what the controller JavaScript file looked like to start out with:
(function () { 'use strict'; angular .module('app') …

Gamify TDD

I like it when things that would not normally be associated with games add concepts from games as a way to incentives you to accomplish things. Why simply go for a run if you can have an app that will track you and give you a gold star if you do better than you did the last time? Why go to the coffee shop that only gives you coffee if the other one will give you points that you can redeem for free drinks eventually?

I was recently introduced to CodeSchool, an online training system similar to PluralSight, it has video courses and challenges you can take to prove that you retained what the video taught. CodeSchool also adds badges and tracks to your learning, so as you complete a video and its challenges you get a badge. Complete a collection of courses within a specific discipline and you become a master of that discipline.

Some of these incentives are not tangible and really don't mean much in the real world, but they tend to work for me. If I start working towards a large goal a…