Just an FYI: Unit testing is not equivalent to TDD. TDD is a process of which unit testing is an element.
With that said, if you were looking to implement unit testing then there's a number of things you could do:
All new code/enhancements are tested
This way you don't have to go through and unit test everything that already exists, so the initial hump of implementing unit testing is much smaller.
Test individual pieces of data
Testing something that can contain large amounts of data can lead to many edge cases and gaps in the test coverage. Instead, consider the 0, 1, many option. Test a 'batch' with 0 elements, 1 element and many elements. In the case of 1 element, test the various permutations that the data for that element can be in.
From there, test the edge cases (upper bounds to the size of individual elements, and quantity of elements in the batch). If you run the tests regularly, and you have long running tests (large batches?), most test runners allow categorization so that you can run those test cases separately (nightly?).
That should give you a strong base.
Using actual data
Feeding in 'actual' previously used data like you're doing now isn't a bad idea. Just complement it with well formed test data so that you immediately know specific points of failure. On a failure to handle actual data, you can inspect the results of the batch process, produce a unit test to replicate the error, and then you're back into red/green/refactor with useful regression cases.
First of all, TDD does not strictly force you to write SOLID code. You could do TDD and create one big mess if you wanted to.
Of course, knowing SOLID principles helps, because otherwise you may just end up not having a good answer to many of your problems, and hence write bad code accompanied by bad tests.
If you already know about SOLID principles, TDD will encourage you to think about them and use them actively.
That said, it doesn't necessarily cover all of the letters in SOLID, but it strongly encourages and promotes you to write at least partly SOLID code, because it makes the consequences of not doing so immediately visible and annoying.
For example:
- You need to write decoupled code so you can mock what you need. This supports the Dependency Inversion Principle.
- You need to write tests that are clear and short so you won't have to change too much in the tests (which can become a large source of code noise if done otherwise). This supports the Single Responsibility Principle.
- This may be argued over, but the Interface Segregation Principle allows classes to depend on lighter interfaces that make mocking easier to follow and understand, because you don't have to ask "Why weren't these 5 methods mocked as well?", or even more importantly, you don't have a lot of choice when deciding which method to mock. This is good when you don't really want to go over the whole code of the class before you test it, and just use trial and error to get a basic understanding of how it works.
Adhering to the Open/Closed principle may well help tests that are written after the code, because it usually allows you to override external service calls in test classes that derive from the classes under test. In TDD I believe this is not as required as other principles, but I may be mistaken.
Adhering to the Liskov substitution rule is great if you want to minimize the changes for your class to receive an unsupported instance that just happens to implement the same statically-typed interface, but it's not likely to happen in proper test-cases because you're generally not going to pass any class-under-test the real-world implementations of its dependencies.
Most importantly, SOLID principles were made to encourage you to write cleaner, more understandable and maintainable code, and so was TDD. So if you do TDD properly, and you pay attention to how your code and your tests look (and it's not so hard because you get immediate feedback, API and correctness wise), you can worry less about SOLID principles, in general.
Best Answer
Writing test code against a concrete type is fine, and it is not a violation of the SOLID principles (or, to be more specific, of the "D" in the SOLID priciples). That is because the purpose of a typical test case is to test exactly that one concrete type it tests, not more, not less. There is no benefit in having the "subject under test" potentially mocked out from the test code. There might be cases where you have different components which all use the same interface, and you want to have a "reusable test" which can be applied to all these implementations, but to my experience these situations are rare and should not bring you to the conclusion unit tests should exclusesively call components via an explicit interface.
On abstractions, yes - but that does not mean all code needs to depend on a explicit interface in the sense of the
interface
keyword in Java or C#.For a typical unit test, to depend on abstractions means simply just to rely on the public members of a class. That is also called the "interface" of a class, but not in the explicit sense mentioned above. That way, one also follows the "Abstractions should not depend on details" (=implementation details like private functions) recommendation of the DIP.