The basic principle at work in testing is that for any given piece of code with N branches, there are subsequently as many as 2N possible paths you can take through that code. That means for some method that might actually be pretty small, the number of ways it can be used can take a lot more to describe than the method itself.
Here's a common scenario:
if (@user.posts.count > @limit.posts)
# Branch 1
flash.now[:error] = "You have posted too many times today."
elsif (@user.suspended?)
# Branch 2
flash.now[:error] = "Your account is suspended. You cannot post."
else
if (@post.special? and [email protected]_special?)
# Branch 3
flash[:warning] = "You cannot create special posts."
@post.special = false
# else
# Branch 4
# Not branching also has to be tested
end
if (@user.recently_created?)
# Branch 5
@post.newbie = true
# else
# Branch 6
end
# Branch 7
end
unless (flash[:error])
@post.save!
end
Although this is pretty straightforward, creating the circumstances that will cause the logic to flow through the correct branches is not. You will probably have to write a test case for each specific one. This is not always trivial, but you can make it easier if you have a number of prepared fixtures or factory methods that construct things nearly ready to go.
Without testing each of these individual cases using automatic tests the chance of a malfunction due to an inadvertent change is significant. To ensure that it's working you will have to manually run through these paths, and it's often that you'll forget one and later this will cause trouble, probably the embarrassing variety.
The way to do less testing is to have lower code complexity by eliminating business logic that doesn't add business value, for instance. Keep only the most imperative things, discard anything superfluous.
If you're writing a thousand different tests for something that should be simple, maybe it's not simple enough in terms of design.