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.
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.
So given these tests I was able to create this function:
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.
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.
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.
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 FunctionI 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
Post a Comment