4

While solving any problem, we write algorithms. Some efficient, some not, some work, some fail. But sometimes we end up writing something which is mostly a success when we do a dry test run, perhaps, the way we frame the test data is prejudiced, but the algorithm fails in some other cases.

For some algorithms , the nature of data can be diverse and magnitude large, for example like the problem:

Find the maximum subsequence sum of an array of integers which contains both positive and negative numbers and return the starting and ending indices within the array.

Can anyone tell me is there any specific and generic thumb rule by which we can design the most stringent of test cases to test the correctness of algorithms like this one ?

4 Answers 4

3

I will assume we are talking about a reasonably complex algorithm, i.e., one where you'd usually get out pen and paper and need quite some time before you arrive at the pseudocode for a solution.

In such a scenario, the most challenging test cases are the edge cases you probably didn't think of. Here, test-driven design can only get you so far, as the test coverage is severely limited by your imagination. In these cases, I try a branch-based approach. I try to create input sequences that will hit all possible control flow variations and assert important properties that help me believe that the algorithm is in fact correct.

The important thing is that for algorithmic problems, there is usually quite a high density of edge cases. It is easier to find out what these edge cases are if you can look at the algorithm, i.e., do whitebox testing. In TDD, you'd do the opposite: ideally, you write all your test cases before you even start implementing the code. However, if the problem was algorithmically challenging, you probably already made sure that your algorithm would return the correct solution for all instances that you managed to come up with upfront.

1
  • +1 "the most challenging test cases are the edge cases you probably didn't think of." But IMHO TDD is really a white-box testing approach - you design your test cases specificially for your code (or your code specifically for your test cases). You don't write "all test cases beforehand" - you write one test case, then some code, and then the next test case for a feature your code does not support so far, then the code for that feature, and so on. And it is always a technique for the one who knows the code by heart.
    – Doc Brown
    Commented Apr 16, 2013 at 6:50
5

While I agree with Robert Harvey's statement, that test driven development is a useful approach, I often do get the impression that people think that with sufficiently many unit tests in place they are on the safe side and are allowed to stop thinking about useful tests. This, I think, is far from being true.

In my opinion creating reasonable tests is something between an art and good craftmansship, where knowing about existing tools and best practices together with thouroghly and correctly applying them is the craftmanship part. Good knowledge about common programming errors as an additional ingredient will also help to find useful tests. But you also have to understand the problem solved by the algorithm and need to get an idea of where an implementation is likely to fail. You will, at least partially, often be on your own here. I'd try to find at least one alternative algorithm and run them concurrently. Often you choose a specific one for performance -- in automated test you may use a slower one for croos checking. Whatever helps is somehting you can put into the best practices box.

Without spending too much time on it, for the problem you gave as an example I'd try to find an (automated) way to (randomly) create (many) sequences of varying length for which I can predict the result. Then feed them into the algorithm, and log the input in case it fails. In addition I'd do that for a fixed (not random) set of sequences as a protection against code changes which might break the code. I'd make sure there will be sequences with more than one solution and try to figure out whether sequences with overlapping solutions exist and if yes, try to create some of them, as well.

I'd like to add that for me, as a mathematician, an algorithm is either correct or wrong for at least one set of input data and it is something for which you can (often) give prove (the link is more or less randomly chosen) -- of course there is always the human factor....But even if you believe that, you cannot do that (give proof) for an implementation of a (sufficiently complex) algorithm, which is the thing which is actually under test.

Edit I think an example which is sufficiently obvious and elaborate at the same time might be useful. For this I'd like to draw your attention to Knuth's 'The art of computer programming' vol 2, where under the heading 'The classical algorithms' an algorithm is presented which shows how to divide an (arbitrary) positive integer by another positive integer. Knuth actually gives (a somewhat hard to understand) proof of why his algorithm is correct. A certain part of that proof shows that there is a straightforward way of performing a certain task in that algorithm and an exceptional case. The exceptional case (see step D3 in algorithm D if you really want to look it up) occurs extremely rarely and, actually, requires a more complex calculation.

Now while there is a quite obvious way to create unit tests for this algorithm (apply it to two integers and then perform the reverse operation to check whether you get to from where you started) it is absolutely not obvious how to make sure the exceptional case is tested. First you need to know about it (that is, you have to understand the hairy details of the proof of the algorithm), then you need to find data for which it will actually occur. Doing the whole chain (finding the algorithm, which is, in this case, done for you by the author, finding a proof for it, identifying the exceptional cases and making sure test data is found which test this as well) is not covered by any kind of best practice.

3
  • 1
    +1 The random approach you suggest (verifying properties against randomly generated samples) actually exists! See tools such as QuickCheck (for Haskell) or Scalacheck for Scala :)
    – Andres F.
    Commented Apr 15, 2013 at 18:09
  • @AndresF. Thanks, but have a look at my edit. How do you ensure the random approach finds cases which are (statistically) extremely rare? I implemented the algorithm for division for bigints and tested it with random data for two days. No failure. Then I added support for the exceptional case, and it failed immediately...
    – Thomas
    Commented Apr 15, 2013 at 18:14
  • Agreed, I think several complementary approaches are needed. And yes, nothing beats actually understanding how the algorithm works and what its special cases are (which is why TDD is hopelessly inadequate for this task)
    – Andres F.
    Commented Apr 15, 2013 at 18:38
2

As @Thomas has pointed out, designing good test cases needs experience. But there is good news for you: you can learn it, and there are a lot of books out there where systematic approaches are described. For example, a "classic" one is "The Art of Software Testing, Second Edition" by Glenford Myers.

Some examples for techniques described there

  • code coverage / branch coverage tests: create a set of test cases so that every line of code is executed, or every possible branch is executed (this is a white-box technique for a program where you have inside into the source code)
  • equivalence class: partition your space of input data into disjoint classes, and create at least one test case for each class
  • edge case analysis: in your example above that may mean to create a test case with an empty array, a one-element array, a two-element array, an array with just positive, just negative elements or just zeros etc.

So, the answer to your question is: there is not just "one" rule of thumb, there are many, and you should have a look into additional literature to learn them.

1

The best way that I know of is to write programs using the Test-Driven-Development methodology. By writing your test cases first, and then only writing code that makes your test cases pass, your algorithms will only implement your test cases, and therefore your code will have 100% coverage.

That's a bit tongue-in-cheek, of course. Writing test cases and code that passes those test cases doesn't guarantee that your code won't have edge behavior beyond the test cases, and I still don't believe that you can simply grow a sensible architecture from the ground up using only red-green-refactor.

The way out is not to find the best possible testing algorithms (although that is certainly a good thing to do). Rather, use best programming practices and design your programs in such a way that the amount of unexpected behavior is minimized. Learning how to do that is the ongoing life and learning of every professional programmer.

You can also use code coverage tools to identify test cases.

6
  • I don't understand why you started your answer with a misleading paragraph, which you later acknowledge to be "tongue in cheek"? Shouldn't you remove it, or at least move the relevant part of your answer to the top? (that TDD isn't the answer to input space partitioning is a given)
    – Andres F.
    Commented Apr 15, 2013 at 16:46
  • @AndresF. Hmm, I thought I was pretty clear. Commented Apr 15, 2013 at 16:56
  • Maybe I'm being a nitpicker, but your most relevant answer is the last line (about Pex). Why not move it to the top, since it's the actual answer? The rest is either wrong (the tongue-in-cheek part) or incomplete ("design your programs in such a way..."). Partitioning the test space is usually at least a whole chapter in software engineering books, and there are published papers about it. Obviously "design your algorithms well" is not the complete answer, but only one side of the equation!
    – Andres F.
    Commented Apr 15, 2013 at 17:10
  • @AndresF. Not sure what you're expecting... As you pointed out, partitioning the test space is out of scope here, and Pex is already in the answer. Is your quibble really the order of the paragraphs? The TDD paragraph is not wrong; it is illustrative. Commented Apr 15, 2013 at 17:13
  • 1
    TDD does not enforce or teach how to write good test cases. And using this experimental "Pex" generator was really disillusioning for me. I agree on all other things you wrote, but I don't think this will help the OP concerning his question. So I am really astonished that the OP has choosen your answer - perhaps he did not notice that you did not really answer his question.
    – Doc Brown
    Commented Apr 16, 2013 at 6:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.