Talvez eu possa lhe dar um gostinho da nossa experiência quando começamos a analisar os testes de unidade de nosso processo de camada intermediária, que incluíam uma tonelada de operações sql de "lógica de negócios".
Primeiro, criamos uma camada de abstração que nos permitia "encaixar" qualquer conexão de banco de dados razoável (no nosso caso, simplesmente suportávamos uma única conexão do tipo ODBC).
Quando isso aconteceu, pudemos fazer algo assim em nosso código (trabalhamos em C ++, mas tenho certeza de que você entendeu):
GetDatabase (). ExecuteSQL ("INSERIR EM FOO (blá, blá)")
Em tempo de execução normal, GetDatabase () retornaria um objeto que alimentava todo o nosso sql (incluindo consultas), via ODBC diretamente no banco de dados.
Começamos então a olhar para os bancos de dados na memória - o melhor parece ser o SQLite. ( http://www.sqlite.org/index.html ). É incrivelmente simples de configurar e usar, e nos permitiu subclassar e substituir GetDatabase () para encaminhar o sql para um banco de dados na memória que foi criado e destruído para cada teste realizado.
Ainda estamos nos estágios iniciais disso, mas parece bom até agora, no entanto, precisamos garantir a criação de quaisquer tabelas necessárias e preenchê-las com dados de teste - no entanto, reduzimos a carga de trabalho aqui criando um conjunto genérico de funções auxiliares que pode fazer muito disso tudo por nós.
No geral, ajudou imensamente em nosso processo TDD, pois fazer o que parece ser mudanças bastante inócuas para corrigir certos bugs pode ter efeitos bastante estranhos em outras áreas (difíceis de detectar) do seu sistema - devido à natureza do sql / bancos de dados.
Obviamente, nossas experiências se concentraram em um ambiente de desenvolvimento em C ++, no entanto, tenho certeza de que talvez você possa obter algo semelhante trabalhando em PHP / Python.
Espero que isto ajude.