O básico
No teste de unidade, não se deve atingir o banco de dados. Eu poderia pensar em uma exceção:atingir um banco de dados na memória, mas mesmo isso já está na área de testes de integração, pois você só precisaria do estado salvo na memória para processos complexos (e, portanto, não realmente unidades de funcionalidade). Então, sim, não DB real.
O que você deseja testar em testes de unidade é que sua lógica de negócios resulta em chamadas de API corretas na interface entre seu aplicativo e o banco de dados. Você pode e provavelmente deve supor que os desenvolvedores da API/driver do banco de dados fizeram um bom trabalho testando que tudo abaixo da API se comporta conforme o esperado. No entanto, você também deseja abordar em seus testes como sua lógica de negócios reage a diferentes resultados de API válidos, como salvamentos bem-sucedidos, falhas devido à consistência de dados, falhas devido a problemas de conexão etc.
Isso significa que o que você precisa e deseja zombar é tudo o que está abaixo da interface do driver do banco de dados. No entanto, você precisaria modelar esse comportamento para que sua lógica de negócios pudesse ser testada para todos os resultados das chamadas de banco de dados.
É mais fácil falar do que fazer porque isso significa que você precisa ter acesso à API por meio da tecnologia que usa e precisa conhecer a API.
A realidade do mangusto
Aderindo ao básico, queremos zombar das chamadas executadas pelo 'driver' subjacente que o mangusto usa. Supondo que seja node-mongodb-native precisamos zombar dessas chamadas. Compreender a interação completa entre o mongoose e o driver nativo não é fácil, mas geralmente se resume aos métodos em
mongoose.Collection
porque o último estende mongoldb.Collection
e não reimplemente métodos como insert
. Se pudermos controlar o comportamento de insert
nesse caso específico, sabemos que zombamos do acesso ao banco de dados no nível da API. Você pode rastreá-lo na fonte de ambos os projetos, que Collection.insert
é realmente o método de driver nativo. Para o seu exemplo específico, criei um um repositório Git público com um pacote completo, mas vou postar todos os elementos aqui na resposta.
A solução
Pessoalmente, acho a maneira "recomendada" de trabalhar com o mangusto bastante inutilizável:os modelos geralmente são criados nos módulos onde os esquemas correspondentes são definidos, mas eles já precisam de uma conexão. Para fins de ter várias conexões para conversar com bancos de dados mongodb completamente diferentes no mesmo projeto e para fins de teste, isso torna a vida muito difícil. Na verdade, assim que as preocupações são totalmente separadas mangusto, pelo menos para mim, torna-se quase inutilizável.
Então, a primeira coisa que crio é o arquivo de descrição do pacote, um módulo com um esquema e um "gerador de modelo" genérico:
{
"name": "xxx",
"version": "0.1.0",
"private": true,
"main": "./src",
"scripts": {
"test" : "mocha --recursive"
},
"dependencies": {
"mongoose": "*"
},
"devDependencies": {
"mocha": "*",
"chai": "*"
}
}
var mongoose = require("mongoose");
var PostSchema = new mongoose.Schema({
title: { type: String },
postDate: { type: Date, default: Date.now }
}, {
timestamps: true
});
module.exports = PostSchema;
var model = function(conn, schema, name) {
var res = conn.models[name];
return res || conn.model.bind(conn)(name, schema);
};
module.exports = {
PostSchema: require("./post"),
model: model
};
Tal gerador de modelo tem suas desvantagens:existem elementos que podem precisar ser anexados ao modelo e faria sentido colocá-los no mesmo módulo onde o esquema é criado. Portanto, encontrar uma maneira genérica de adicioná-los é um pouco complicado. Por exemplo, um módulo pode exportar pós-ações para serem executadas automaticamente quando um modelo é gerado para uma determinada conexão etc. (hacking).
Agora vamos zombar da API. Vou mantê-lo simples e apenas zombar do que eu preciso para os testes em questão. É essencial que eu gostaria de simular a API em geral, não métodos individuais de instâncias individuais. O último pode ser útil em alguns casos, ou quando nada mais ajuda, mas eu precisaria ter acesso a objetos criados dentro da minha lógica de negócios (a menos que injetados ou fornecidos por meio de algum padrão de fábrica), e isso significaria modificar a fonte principal. Ao mesmo tempo, zombar da API em um só lugar tem uma desvantagem:é uma solução genérica, que provavelmente implementaria uma execução bem-sucedida. Para testar casos de erro, pode ser necessário simular instâncias nos próprios testes, mas, em sua lógica de negócios, você pode não ter acesso direto à instância de, por exemplo,
post
criado no fundo. Então, vamos dar uma olhada no caso geral de simulação de chamada de API bem-sucedida:
var mongoose = require("mongoose");
// this method is propagated from node-mongodb-native
mongoose.Collection.prototype.insert = function(docs, options, callback) {
// this is what the API would do if the save succeeds!
callback(null, docs);
};
module.exports = mongoose;
Geralmente, desde que os modelos sejam criados depois modificando o mangusto, é pensável que as simulações acima sejam feitas por teste para simular qualquer comportamento. Certifique-se de voltar ao comportamento original, no entanto, antes de cada teste!
Finalmente, é assim que nossos testes para todas as possíveis operações de salvamento de dados podem se parecer. Preste atenção, eles não são específicos do nosso
Post
model e poderia ser feito para todos os outros modelos com exatamente o mesmo mock no lugar. // now we have mongoose with the mocked API
// but it is essential that our models are created AFTER
// the API was mocked, not in the main source!
var mongoose = require("./mock"),
assert = require("assert");
var underTest = require("../src");
describe("Post", function() {
var Post;
beforeEach(function(done) {
var conn = mongoose.createConnection();
Post = underTest.model(conn, underTest.PostSchema, "Post");
done();
});
it("given valid data post.save returns saved document", function(done) {
var post = new Post({
title: 'My test post',
postDate: Date.now()
});
post.save(function(err, doc) {
assert.deepEqual(doc, post);
done(err);
});
});
it("given valid data Post.create returns saved documents", function(done) {
var post = new Post({
title: 'My test post',
postDate: 876543
});
var posts = [ post ];
Post.create(posts, function(err, docs) {
try {
assert.equal(1, docs.length);
var doc = docs[0];
assert.equal(post.title, doc.title);
assert.equal(post.date, doc.date);
assert.ok(doc._id);
assert.ok(doc.createdAt);
assert.ok(doc.updatedAt);
} catch (ex) {
err = ex;
}
done(err);
});
});
it("Post.create filters out invalid data", function(done) {
var post = new Post({
foo: 'Some foo string',
postDate: 876543
});
var posts = [ post ];
Post.create(posts, function(err, docs) {
try {
assert.equal(1, docs.length);
var doc = docs[0];
assert.equal(undefined, doc.title);
assert.equal(undefined, doc.foo);
assert.equal(post.date, doc.date);
assert.ok(doc._id);
assert.ok(doc.createdAt);
assert.ok(doc.updatedAt);
} catch (ex) {
err = ex;
}
done(err);
});
});
});
É essencial observar que ainda estamos testando a funcionalidade de nível muito baixo, mas podemos usar essa mesma abordagem para testar qualquer lógica de negócios que use
Post.create
ou post.save
internamente. A parte final, vamos executar os testes:
> [email protected] test /Users/osklyar/source/web/xxx
> mocha --recursive
Post
✓ given valid data post.save returns saved document
✓ given valid data Post.create returns saved documents
✓ Post.create filters out invalid data
3 passing (52ms)
Devo dizer que não é divertido fazê-lo dessa maneira. Mas desta forma é realmente puro teste de unidade da lógica de negócios sem nenhum banco de dados real ou na memória e bastante genérico.