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 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'

Interns: Taking off the training wheels

My intern team has been working for several weeks now on our new website. We have already completed one deployment to production and are finalizing our second one. We started with a plan to release often adding small bits of functionality as we go and so far that plan has been working really well. We already feel like we have accomplished a lot because we have completed many of our project's requirements and should easily be able to complete the rest giving us time to do even more than just the original requirements. One of the things I have had some difficulty balancing has been how much to lead the interns and how much to let them figure out on their own. In deciding what our team process should be and how we should allocate our time, I think it was important for me to do more leading. I saw some deficiencies in how we were currently working and brought up some ideas for how we could address them. We had moved into spending all our time just working through stories and did not

My idea for Hearthstone to add more deck slots

Recently someone asked the Blizzard developers for more slots for decks in the game Hearthstone. The response was that they are talking about it and looking into it, but no decision has been made yet. One of the concerns over adding deck slots is that it could complicate the UI for Hearthstone and make it more difficult for new players to understand. I have what I think would be a good solution to add more deck slots without increasing the learning curve for the game much if at all. First I would take a look at the current selection screen for starting to play a game. It defaults to showing the decks that are custom built by the player if they have any custom decks, and there is an option to page over to the basic decks. This basic deck screen is perfect for how I would change this process. Instead of having 2 pages of decks, 1 for basic and 1 for custom, you would just see the select a Hero screen. Then once you selected the Hero you wanted, you would see all of the decks that