Meu primeiro artigo sobre desenvolvimento .Net, publicado na MSDN Brasil Magazine, ensinava como criar um Windows service em .Net. Na época o assunto era uma grande novidade. Mais de quatro anos , no entanto, até hoje vejo perguntas a este respeito no Fórum da MSDN ou em grupos de discussões. Por isto, decidi escrever uma nova versão, mais moderna, mas que basicamente cobre o mesmo assunto.
A primeira diferença é o formato, como estou publicando no blog, vou picotar o artigo em pequenas cápsulas mais fáceis de digerir. Além disto, vou inverter totalmente a ordem que fiz antes e que vejo em todo artigo sobre Windows Service. Ao invés de começar montando toda a infra-estrutura do serviço, vou começar implementando e testando nossas necessidades de negócio. Afinal de contas esta parte é mais importante. O serviço, na realidade, é apenas um meio para atingirmos nossos objetivos.
Existem dois requisitos que são os favoritos de quem precisa criar um serviço:
- Monitorar um diretório, identificar a criação/alteração de um arquivo e processá-lo
- Executar um processo periodicamente
Então vamos começar implementando um monitor de diretórios. Sua função é monitorar um diretório, avaliando os eventos acontecidos e processar arquivos que são criados ou alterados neste diretório. Um monitor como este precisa saber qual diretório deve monitorar, o nome do arquivo, ou uma expressão regular para nomes de arquivos variáveis. Além disto, deve possibilitar iniciar e parar o monitor. Isto é bem simples usando o FileSystemWatcher, mesmo assim vou fazer uma classe utilitária para isolar este contato com uma API de infra-estrutura. A Listagem 1 mostra como fica esta classe.
Listagem 1: Isolando o contato com o file system
public class FolderWatcher
{
private FileSystemWatcher _folderWatcher;
private bool _isWatching = false;
private IProcessFile _processFile;
public bool isWatching
{
get { return _isWatching; }
}
public void WatchFolder(string _folderPath, string _filter, IProcessFile processFile)
{
_processFile = processFile;
_folderWatcher = new FileSystemWatcher(_folderPath, _filter);
_folderWatcher.NotifyFilter = NotifyFilters.FileName;
_folderWatcher.Created += OnCreated;
_folderWatcher.EnableRaisingEvents = true;
_isWatching = true;
}
public void StopWatching()
{
_folderWatcher = null;
_isWatching = false;
}
private void OnCreated(object sender, FileSystemEventArgs e)
{
_processFile.Process(e.FullPath);
}
A classe é simples e direta, oferece apenas dois métodos: WatchFolder() e StopWatching(). O primeiro recebe como parâmetros o endereço do diretório a monitorar e o filtro para o nome do arquivo, que pode ser uma expressão regular. Por último ele recebe uma interface, que será responsável pro processar o arquivo. Esta é uma forma de abstrair desta classe o processamento do arquivo.
Meu problema agora é testar esta classe. Existem duas opções: Criar mockings para simular o comportamento da classe FileSystemWatcher ou simular a criação do arquivo em um diretório. Apesar de a segunda opção ser menos ortodoxa do ponto de vista de testes unitários, pois não isola o teste, vou optar por ela. O motivo é simples, a classe FolderWatcher praticamente não tem comportamento próprio, desta forma não vejo grandes ganhos em testá-la isoladamente.
Listagem 2: Testando o FolderWatcher
[TestFixture]
public class FolderWatcherTest
{
private string _testFolderPath;
private string _randomFileName;
private string _filePath;
[SetUp]
public void Setup()
{
_testFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
_randomFileName = Path.GetRandomFileName();
_filePath = string.Format(@"{0}\{1}", _testFolderPath, _randomFileName);
}
[Test]
public void Inicia_E_CriaArquivo()
{
MockRepository mockery = new MockRepository();
string filter = _randomFileName;
IProcessFile mockedProcessFile = mockery.CreateMock<IProcessFile>();
Expect.Call(delegate { mockedProcessFile.Process(_filePath); }).Repeat.Once();
mockery.ReplayAll();
FolderWatcher folderWatcher = new FolderWatcher();
folderWatcher.WatchFolder(_testFolderPath, filter, mockedProcessFile);
CreateDummyFile();
Wait(1000);
mockery.VerifyAll();
}
[TearDown]
public void TearDown()
{
ClearFile();
}
private void CreateDummyFile()
{
TextWriter writer = new StreamWriter(_filePath);
writer.Write("qualquer texto");
writer.Flush();
writer.Close();
}
private void ClearFile()
{
File.Delete(_filePath);
}
private void Wait(int timeout)
{
System.Threading.Thread.Sleep(timeout);
}
}
A Listagem 2 mostra um teste que inicia o meu FolderWatcher e testa o monitor, criando um arquivo no diretório monitorado. Esto utilizando a ferramenta de mock, Rhino Mocks, para abstrair a classe que na vida real vai processar o arquivo recém criado. Defino que o método Process() será chamado somente uma vez, com o caminho para o arquivo como argumento.
Tive que fazer um pequeno hack, que é o método Wait. Isto não é muito bom, pois cria mais uma variável de falha no teste, mas necessário, porque o FileSystemWatcher não reage imediatamente a criação do arquivo. Preciso esperar um segundo para a coisa toda funcionar. Provavelmente existe uma forma melhor de fazer isto, sugestões são bem-vindas.
Este é o teste mais importante, mas para garantir a qualidade da classe é necessário testar outros cenários, como enviar um endereço de diretório inválido, ou criar o arquivo com o FolderWatcher parado. Quanto mais cenários testados, melhor.
Próximo capítulo vamos implementar outra função básica necessária que é a execução de processo periódico que irá rodar em background, sem a interação de um usuário para iniciá-lo.
Antes que eu me esqueça, vou liberar o código também aos poucos, evoluindo a cada capítulo. Pode pegar sua cópia aqui.