Bigger tests than code?
Sometimes the test will be more complex.
Sometimes the code will be more complex.
Refactoring to Simplify Code and Test Cases
It may be worth considering some simplifications and refactoring if things are getting exponential on you.
I have worked with several maintenance projects that were littered with God classes. These tended to be much more manageable when refactored into smaller classes.
Some classes are not minimal and a search for repeated code, and refactoring to eliminate repetition has the potential to reduce your test surface significantly.
Black Box vs. Glass Box Testing
One issue that arises is that of TDD methodology. When the user story changes, that produces more tests. On initial execution, they fail, and more code is written to make them pass. When they pass, the user story is implemented and we ship. Naturally, in real life, things might not be this pure. However, I guess it might be worth asking how much does the user story elaborate the test cases? Are the test cases functional, black box testing based on exercising the valid and invalid inputs?
It sounds from your description that some of the test cases are aimed at statement coverage. I like statement coverage, but there are several additional kinds of coverage as well. There are methods to optimize coverage test cases that you should perhaps consider if you find that part of your high test case count is due to brute forcing things to get through to every line.
Code Coverage Types
The following link illustrates with some cool graphics several types of coverage. I don't endorse or have an opinion about their product, just their diagram.
In their example, they show modified condition/decision coverage and determine that for the code they show, four test cases both execute every statement, and cause the branching into the if statement for every kind of condition that might occur. Discovery of test cases when the decision criteria are nested comparisons like
if (a < 5) // d1
if (b < 10) //d2
a = 7;
if ((a + b) < 8) //d3
c = 5;
can be trickier to analyze for test case values, but you know you will need a test case with a < 5 and a >=5 from the very first if, and also b < 10 and b >= b from the second if.
Because in some paths a will be the input, and others a will be a modified value (a=7), you may need to use a technique called symbolic execution where the third if is analyzed in terms of a' and a''. To execute c = 5, you will need a test case where the condition is satisfied. You will also want to run a test where c is never assigned.
A list of test cases that I think covers everything is:
T1: a=4, b=9 => a'=4, a''=7, covers d1=true, d2=true, d3=false
T2: a=5, b=9, covers d1=false
T3: a=4, b=10, covers d2= true
But how do we get d3 covered?
Assigning a in the second if statement makes the analysis more complicated unless we do this with it.
((a' + b) < 8), ((a'' + b) < 8)
((a' + b) < 8), ((7 + b) < 8) // Remember a'' = 7
((a' + b) < 8), (b < 1) // Remember a'' = 7
T4: a=1, b=0, covers d3=true
Symbolic execution is a test related technique that you can use to reduce the test cases needed to perform decision coverage. It is not an easy read, but a classic paper on the subject is:
Apologies in advance if this diverts too far from your original question. I hope it will be of some use.