No TDD, vou testar as unidades juntas quando fizer sentido. Do jeito que eu vejo, eu uso mock / stubs por dois motivos: o comportamento do sistema se torna muito complexo para testar efetivamente com a implementação completa, e a implementação completa pode fazer coisas que eu não quero que aconteçam.
Basicamente, quanto mais objetos estiverem envolvidos em um teste, mais difícil será o sistema prever. Você tem a previsão do comportamento do sistema para escrever o teste. Alguns objetos podem ter um comportamento complexo e pode ser difícil induzir casos de borda específicos em uma unidade. Assim, pode ser realmente útil zombar desses objetos.
Em outros casos, os objetos têm efeitos colaterais indesejáveis. Suponha que você tenha um CreditCardProcessor. Você não quer realmente cobrar cartões de crédito enquanto seus testes estão sendo executados. Outros itens, como desenhar gráficos ou acessar recursos da web, podem estar na mesma categoria.
Quando um objeto tem uma dependência, como você decide incluir o objeto real ou algum tipo de simulação / stub?
Primeiramente, se houver alguma possibilidade de que o comportamento do objeto mude durante o desenvolvimento, eu o farei. Por exemplo, considere uma classe de fila de prioridade versus uma classe de estratégia de preço. Uma fila de prioridades quase sempre sempre manterá o mesmo comportamento. No entanto, sua estratégia de preços provavelmente mudará muito. Como resultado, você não quer que outros testes dependam do comportamento dentro da estratégia de preços. Se o fizerem, você acabará quebrando outros testes desnecessariamente. No entanto, não é realmente um grande problema para a fila de prioridades, porque o comportamento nunca deve mudar.
Em segundo lugar, como "gordura" é a interface entre os objetos? Se os objetos tiverem uma interface muito simples, então o mocking é fácil e eu farei isso. Se os objetos tiverem uma interface complexa, o escárnio será difícil e menos provável de valer a pena. Nesse caso, vamos contrastar um objeto de conexão com o banco de dados e uma estratégia de preço. A interface de estratégias de preço deve ser razoavelmente simples, esperamos que seja apenas um método CalculatePrice (SalesOrderItem). Claro, o código real pode fazer todo tipo de coisa com o SalesOrderItem, mas seu stub não precisa lidar com isso. Por outro lado, uma conexão de banco de dados tem instruções SQL sendo passadas para ele, o que lhe dá uma interface bastante complexa. Zombar do banco de dados é realmente difícil porque você verifica todas as consultas que estão sendo feitas e fornece uma resposta correta. Além disso, você não está verificando se as consultas são válidas (apenas que elas correspondem ao que você espera), nesses casos, verificar um banco de dados real faz mais sentido, dessa forma você realmente verifica se as consultas funcionam e os testes ainda passar se você reescrever as consultas para dar os mesmos resultados, mas de uma maneira diferente.
Em terceiro lugar, se um objeto for lento, eu o toco. Se você tiver um banco de dados, as chamadas para ele serão bastante lentas, solicitando o uso de um stub para evitar ter que chamá-lo. Semelhante para acesso à web, etc.
Eu apenas usei bancos de dados como um exemplo de algo que você deve copiar e também não stub. Eu stub meus bancos de dados com um banco de dados em memória sqlite que evita o problema de desempenho, mas ainda permite que meu SQL seja testado. Eu estou realmente usando um framework que gera SQL específico para o meu banco de dados para mim, então isso é trabalho.
No seu caso real, você afirma:
The majority of our unit tests are behavioural tests which often became false negative (false red) during refactoring (just because some sequence of dependencies calls changed).
Pelo que entendi, seus testes falham porque antes foo () era chamado primeiro e depois bar (). Agora bar () é chamado então foo (). Se a ordem de chamar foo () e bar () não importa, seus testes não devem verificar qual chamada primeiro. Seu teste deve apenas verificar se ambos são chamados.