It’s hard and unrealistic to maintain large mock data. It’s is even harder when database structure undergoes changes.
False.
Unit testing doesn't require "large" mock data. It requires enough mock data to test the scenarios and nothing more.
Also, the truly lazy programmers ask the subject matter experts to create simple spreadsheets of the various test cases. Just a simple spreadsheet.
Then the lazy programmer writes a simple script to transform the spreadsheet rows into unit test cases. It's pretty simple, really.
When the product evolves, the spreadsheets of test cases are updated and new unit tests generated. Do it all the time. It really works.
Even with MVVM and ability to test GUI, it’s takes a lot of code to reproduce the GUI scenario.
What? "Reproduce"?
The point of TDD is to Design things for Testability (Test Drive Development). If the GUI is that complex, then it has to be redesigned to be simpler and more testable. Simpler also means faster, more maintainable and more flexible. But mostly simpler will mean more testable.
I have experience that TDD works well if you limit it to simple business logic. However complex business logic is hard to test since the number of combination of test (test space) is very large.
That can be true.
However, asking the subject matter experts to provide the core test cases in a simple form (like a spreadsheet) really helps.
The spreadsheets can become rather large. But that's okay, since I used a simple Python script to turn the spreadsheets into test cases.
And. I did have to write some test cases manually because the spreadsheets were incomplete.
However. When the users reported "bugs", I simply asked which test case in the spreadsheet was wrong.
At that moment, the subject matter experts would either correct the spreadsheet or they would add examples to explain what was supposed to happen. The bug reports can -- in many cases -- be clearly defined as a test case problem. Indeed, from my experience, defining the bug as a broken test case makes the discussion much, much simpler.
Rather than listen to experts try to explain a super-complex business process, the experts have to produce concrete examples of the process.
TDD requires that requirements are 100% correct. In such cases one could expect that conflicting requirements would be captured during creating of tests. But the problem is that this isn’t the case in complex scenario.
Not using TDD absolutely mandates that the requirements be 100% correct. Some claim that TDD can tolerate incomplete and changing requirements, where a non-TDD approach can't work with incomplete requirements.
If you don't use TDD, the contradiction is found late under implementation phase.
If you use TDD the contradiction is found earlier when the code passes some tests and fails other tests. Indeed, TDD gives you proof of a contradiction earlier in the process, long before implementation (and arguments during user acceptance testing).
You have code which passes some tests and fails others. You look at only those tests and you find the contradiction. It works out really, really well in practice because now the users have to argue about the contradiction and produce consistent, concrete examples of the desired behavior.
What you actually want to test here, I assume, is that given a specific set of results from the randomiser, the rest of your method performs correctly.
If that's what you're looking for then mock out the randomiser, to make it deterministic within the realms of the test.
I generally have mock objects for all kinds of non-deterministic or unpredictable (at the time of writing the test) data, including GUID generators and DateTime.Now.
Edit, from comments: You have to mock the PRNG (that term escaped me last night) at the lowest level possible - ie. when it generates the array of bytes, not after you turn those into Int64s. Or even at both levels, so you can test your conversion to an array of Int64 works as intended and then test separately that your conversion to an array of DateTimes works as intended. As Jonathon said, you could just do that by giving it a set seed, or you can give it the array of bytes to return.
I prefer the latter because it won't break if the framework implementation of a PRNG changes. However, one advantage to giving it the seed is that if you find a case in production that didn't work as intended, you only need to have logged one number to be able to replicate it, as opposed to the whole array.
All this said, you must remember that it's called a Pseudo Random Number Generator for a reason. There may be some bias even at that level.
Best Answer
TDD is not a substitute for design.
This is a common misconception about TDD, that you can somehow "grow" a coherent design from red-green-refactor. You can't. Your tests still need to be guided by your design skills.
If you're trying to figure out what to do between the input and output of your method under test, it probably means that you're trying to do too much in a single method. Think about what those intermediate steps should do. Write tests for those intermediate steps, and write methods that pass those tests.