Essentially, the story behind these articles is that I like to solve algorithms within my own native code editor and so my process would be to write out each test case, testing each one individually by commenting and uncommenting the function calls. One day I got tired of manually doing that because it took way more time than it needed to and I wondered if there was a way to automate the process. I’m already practicing algorithms whose purpose is the create the simplest solutions to complex problems yet in my very own code I’ve been proving to myself that I didn’t quite get it, I was just practicing for the sake of practicing not exhibiting the actual methodology behind the purpose of coding patterns. I was practicing to complete code challenges. I was missing the point as I wasn’t actually putting those skills into practice. Well, I decided I wanted that to change and these articles are to document the journey.
In this article, I will:
- I will choose and review the example algorithm I will use to demonstrate my testing methods.
For this problem, we need to create a function that will test whether the characters in the first input string, firstStr, are contained in order within the second input string, secondStr, and returning True or False depending on whether it is or isn’t.
- Lines 8 & 9 are testing whether the first input is longer than the second input in which case there is no possibility of the second input containing the first and so we return False. These lines will also take a case where an empty string is given as the first input. Normally this would return True because the second input is a valid string. In our case, we don’t want an empty string to be valid so we’ll return False.
- Lines 11 & 12, the variables secondStrChar and firstStrChar, are tracking which characters in firstStr and secondStr we are currently comparing.
- Then lines 14 through 24 we are looping through both string inputs until one of the pointers (lines 11 & 12) exceeds the length of its string, at which point each character in firstStr has been found in secondStr or secondStr has been fully enumerated through before firstStr.
- Lines 21 through 24 conditionally return True or False depending on which string was enumerated through first. And we know this by the positions of the pointers on lines 11 & 12.
If you want to try this problem for yourself, I took inspiration for it from this problem on LeetCode.
Now that I explained the problem we will be testing solutions to, let’s get into the tests.
Obviously, commenting and uncommenting each test, executing the function, and moving on until all the tests have been passed is not optimal so let’s add some automation.
The first and simplest thing we can do is just wrap all our tests into a function sandwich and then call that function at the bottom of our file.
- Here we create a function that wraps around all of our tests, and when we run this file with Python3 this function will be called at the end, effectively calling isSubsequence against each one of our tests.
- This is the printed returns of isSubsequence being called against each test. To be able to tell if we got the proper return, we have to line up each return in our console to each function call in our file, comparing the console returns to the expected results commented below each call.
As you can tell, so far we have only increased our complexity by adding additional lines of code and making it harder to tell if a particular test gave us the desired result. To reduce this complexity we should look for a way to reduce the lines of code we use in our testing and print statements in the console which clarify the results for the user.
One good initial step to this end is to create a helper function that compares our input to the expected result and then returns a conditional message. That way we can make use of and remove our comments and effectively cut our test code complexity in half.
Ok, I know I have arrows everywhere here but I promise that not a lot has changed. I just created a function runTest to call isSubsequence and compare its return, result to the one I expect, expectedResult.
- I created a function runTest which will take the arguments I want to run against isSubsequence and the output I am expecting to be returned and returning a message based on if the expected output matches the actual output.
- The input I am passing is in an array just because there are multiple arguments to access for isSubsequence. The output is a boolean because that is the output I am expecting from isSubsequence based on the input.
- As alluded to in the previous steps, we will name our parameters input to represent the value we will pass to isSubsequence and expectedResult to represent the value that should be returned from isSubsequence if isSubsequence is working correctly. When we save the result of isSubsequence we have to remember that our input values are being passed in as an array and so we’ll have to access the values as such.
- After we have a value store to result we will compare result against expectedResult and conditionally return a message to the console (i.e. “Test Passed!” Or “Result did not match Expected Result”).
- We have our console message showing that each test passed, meaning that our solution works properly, however, there is no way to tell which test these messages each belong to.
This is good but we repeat ourselves a lot in our code so we can make this much tidier by finding a way to remove all the print statements and the calls to runTest.
- I put all the tests into a hash. Hashes are ideal because you’ll always have an unknown number of tests across problems and this prevents the complexity from getting out of hand. Also, the argument list is usually static. Lastly, a hash is ideal because you can identify a test by its unique key.
- Enumerate through tests hash separating out the key to use as testId, the array containing the inputs for isSubsequence (which we were already passing to runTest as an array so we don’t have to change anything), and lastly our expectedResult.
- We take the key from tests, add an additional parameter runTest which we’ll call testId, and pass the key into testId. We’ll use testId to identify the test our console messages are referring to so we don’t have to go look for it.