SQL Server

Testes de unidade e de integração dos pacotes do SSIS

Pavle Guduric

Baixar o código de exemplo

Trabalhei em um projeto onde construímos o extrato, transformação e carregamento (ETL) processos com mais de 150 pacotes. Muitos deles continha transformações complexas e lógica de negócios, foram, portanto, não simples "mover dados do ponto ao ponto B" pacotes. Fazer pequenas alterações não era simples e os resultados eram muitas vezes imprevisíveis. Para testar os pacotes, nós costumávamos preencher tabelas de entrada ou de arquivos com dados de teste, execute o pacote ou a tarefa no Microsoft Business Intelligence Development Studio (BIDS), escrever uma consulta SQL e compare a saída produzida pelo pacote com o que pensamos que era a saída correta. Mais frequentemente nós acabou de todo o processo ETL em um banco de dados de amostra e amostra apenas os dados de saída no final do processo — um procedimento demorado e pouco confiável. Infelizmente, esta é uma prática comum entre os desenvolvedores de SQL Server Integration Services (SSIS). Ainda mais desafiador é determinar os efeitos que a execução de um pacote tem nos pacotes subseqüentes. Como você construir o seu processo ETL, é criar uma rede de pacotes conectadas e recursos diferentes. É difícil manter uma visão completa das numerosas dependências entre todos estes em todos os momentos.

Este artigo explica como realizar a unidade e testes de integração de pacotes SSIS introduzindo uma biblioteca chamada SSISTester, que é construído no topo da API gerenciada do SSIS. Depois de ler este artigo você deve ser capaz de usar as técnicas descritas e ferramentas para automatizar a unidade e testes de integração de seus projetos novos e existentes do SSIS. Para entender o artigo, você deve ter experiência anterior com o SSIS e c#.

SSISTester

Quando eu comecei a pensar sobre uma estrutura de testes para os pacotes do SSIS, encontrei três aspectos importantes. Primeiro, eu queria ter um semelhante UX para escrever testes usando a estrutura de testes de Visual Studio , então a metodologia típica envolvendo instalação, verificação e limpeza (aka subdivisão) passos tinham que ser aplicada. Em segundo lugar, eu queria usar ferramentas existentes e comprovadas para escrever, executar e gerenciar testes. Mais uma vez, Visual Studio foi a escolha óbvia. E terceiro, eu queria ser capaz de testes de código em c#. Com isso em mente eu escrevi SSISTester, uma biblioteca .NET que paira sobre o tempo de execução SSIS e expõe uma API que permite-lhe escrever e executar testes para os pacotes do SSIS. Os principais componentes lógicos da biblioteca são retratados em Figura1.

Logical Components of the SSISTester Library
Figura 1 componentes lógicos da biblioteca SSISTester

O repositório de pacotes é usado para armazenar crus representações XML de pacotes do alvo. Cada vez que um teste é executado, uma nova instância da classe Microsoft.SqlServer.Dts.Runtime.Package é desserializada a partir de XML com todos os campos e propriedades definidas para seus valores padrão. Isto é importante porque você não quer diferentes testes que visam o mesmo pacote para reutilização acidentalmente qualquer um dos valores definidos pelos testes anteriores.

Instâncias de classes de teste são armazenadas dentro do repositório do teste. Essas classes contêm métodos que implementam seus casos de teste. Quando um teste é executado, esses métodos são chamados pelo mecanismo de teste. As regras específicas que devem ser seguidas durante a criação de classes de teste serão descritas em detalhes mais tarde.

Metadados contém os atributos necessários para decorar uma classe de teste, então pode ser reconhecido como uma implementação de teste. O mecanismo de teste procura por esses atributos quando testes de carregamento no repositório do teste.

O contexto do teste representa um conjunto de classes que fornecem acesso a informações de tempo de execução durante as diferentes fases da execução do teste. Por exemplo, você pode usar essas classes para acessar os diferentes aspectos de um pacote que está sendo testado, como variáveis, propriedades, restrições anteriores, gerenciadores de conexões, em execução de tarefa, erros de pacote e assim por diante.

O mecanismo de teste refere-se as principais classes e interfaces da API do SSISTester que utilizam diretamente o tempo de execução gerenciado do SSIS. Eles são usados para carregar pacotes e classes de teste em seus repositórios respectivos, bem como para executar testes e criar resultados de teste.

Mini ETL

Para criar pacotes e classes de teste, usarei Visual Studio 2012 e SQL Server 2012, e eu vou usar três pacotes para ilustrar um cenário ETL simples no qual o cliente dados, entregados como um arquivo de texto, são transformados e armazenados dentro de um banco de dados. Os pacotes são CopyCustomers.dtsx, LoadCustomers.dtsx e Main.dtsx. CopyCustomers.dtsx copia o arquivo de Customers.txt de um local para outro, e no caminho ele converte todos os nomes de clientes em maiúsculas, texto. Customers.txt é um simples arquivo CSV que contém ids e nomes dos clientes, da seguinte forma:

id,name
1,company1
5,company2
11,company3

LoadCustomers.dtsx carrega os nomes convertidos em banco de dados Demo. Antes ele carrega dados em uma tabela de destino chamada clientes­Staging, ele trunca todos os dados armazenados anteriormente. No final do processo, ele armazena o número de clientes em uma variável. Aqui está o script para criar o banco de dados Demo e a tabela de CustomersStaging:

    CREATE
    DATABASE [Demo]
    GO
    USE [Demo]
    GO
    CREATE TABLE [dbo].[CustomersStaging](
      [Id] [int] NULL,
      [Name] [nvarchar](255) NULL
    ) ON [PRIMARY]
    GO

O pacote Main.dtsx contém duas tarefas executar pacote executar os sub-packages CopyCustomers.dtsx e carga­Customers.dtsx, respectivamente. Gerenciadores de conexões em ambos os CopyCustomers.dtsx e LoadCustomers.dtsx são configuradas usando expressões e variáveis do pacote. As mesmas variáveis de pacote são obtidas a configuração do pacote pai quando executado a partir de outro pacote.

Criando testes de unidade

Para começar, crie um projeto console e adicionar referências de assembly para SSIS.Test. dll e SSIS.Test.Report.dll. Eu vou criar um teste de unidade para o pacote de CopyCustomers.dtsx primeiro. Figura 2 mostra o fluxo de controle (à esquerda) e o fluxo de dados (à direita) para CopyCustomers.dtsx.

Control Flow (Left) and Data Flow (Right) of the CopyCustomers.dtsx Package
Figura 2 controle fluxo (à esquerda) e fluxo de dados (direita) do pacote CopyCustomers.dtsx

Cada teste de unidade é implementada em uma única classe que deriva da classe BaseUnitTest e deve ser decorada com o atributo UnitTest:

[UnitTest("CUSTOMERS", "CopyCustomers.dtsx")]
public class CopyCustomersTest : BaseUnitTest{
  protected override void Setup(SetupContext context){}
  protected override void Verify(VerificationContext context){}
  protected override void Teardown(TeardownContext context){}
}

As marcas do atributo UnitTest uma classe como uma unidade testar a implementação para que ele pode ser encontrado pelo mecanismo de teste. O primeiro parâmetro corresponde ao pacote repositório onde um pacote de destino será carregado durante a execução do teste, os clientes neste exemplo. O segundo parâmetro pode ser o nome de um pacote de destino, o caminho para uma tarefa de fluxo de controle, o caminho para um manipulador de evento ou o caminho para uma restrição anterior. Neste exemplo, é o nome do pacote CopyCustomers.dtsx... porque quero testar o pacote inteiro. Basicamente, o atributo UnitTest dirá ao mecanismo de teste para olhar para o pacote de CopyCustomers.dtsx no repositório clientes e executá-lo durante o teste de CopyCustomersTest.

BaseUnitTest que todas as implementações de teste de unidade precisam derivar de classe base contém três métodos que devem ser implementadas: Configurar, verificar e subdivisão.

Esses três métodos são executados durante as fases de teste diferentes. O método Setup é executado antes que um pacote de destino é executado pelo mecanismo de teste. Configuração prepara o pacote e todas as entradas e saídas que do pacote depende então pode com sucesso ser validado e executado. No exemplo a seguir, eu definir os caminhos para as variáveis do pacote que são usadas como seqüências de conexão em gerenciadores de conexões:

protected override void Setup(SetupContext context){
  if(File.Exists(@"C:\TestFiles\Archive\Customers.txt"))
    File.Delete(@"C:\TestFiles\Archive\Customers.txt");
  if(File.Exists(@"C:\TestFiles\Converted\Customers.txt"))
    File.Delete(@"C:\TestFiles\Converted\Customers.txt");
  DtsVariable sourceFile = context.Package.GetVariable("SourcePath");
  sourceFile.SetValue(@"\\nc1\Customers\Customers.txt");
  DtsVariable destinationFile = 
    context.Package.GetVariable("DestinationPath");
  destinationFile.SetValue(@"C:\TestFiles\Archive\Customers.txt");
  DtsVariable convertedFile = 
    context.Package.GetVariable("ConvertDestinationPath");
  convertedFile.SetValue(@"C:\TestFiles\Converted\Customers.txt");
}

Depois que o método de instalação foi executado com êxito, o mecanismo de teste executa o pacote de destino. Quando o pacote foi executado, mecanismo de teste chama o método Verify e posso verificar se minhas afirmações são verdadeiras:

protected override void Verify(VerificationContext context){
  Assert.AreEqual(true, 
    context.Package.IsExecutionSuccess);
  Assert.AreEqual(true, 
    File.Exists(@"C:\TestFiles\Archive\Customers.txt"));
  Assert.AreEqual(true, 
    File.Exists(@"C:\TestFiles\Converted\Customers.txt"));
  string[] lines = 
    File.ReadAllLines(@"C:\TestFiles\Converted\Customers.txt");
  Assert.AreEqual("COMPANY2", lines[2].Split(',')[1]);
}

O assert primeiro verifica se o pacote foi executado com êxito. O segundo que determina se a tarefa de sistema FST Copie o arquivo de origem arquivo copiado o arquivo \\nc1\Customers\Customers.txt para a pasta C:\TestFiles\Archive\. Os dois últimos afirma validar se o DFT converter nomes de clientes fluxo de dados nomes de empresa tarefa corretamente convertida em maiúsculas. Anteriormente, descrevi brevemente o contexto do teste. Aqui você pode ver como eu usei o parâmetro de contexto para acessar um objeto de pacote dentro os métodos Setup e verifique.

No final do teste, uso o método Teardown para excluir os arquivos que foram copiados ou criados pelo pacote:

protected override void Teardown(TeardownContext context){
  File.Delete(@"C:\TestFiles\Archive\Customers.txt");
  File.Delete(@"C:\TestFiles\Converted\Customers.txt");
}

Tarefas de fluxo de controle de teste

Testes podem direcionar tarefas específicas no fluxo de controle também. Por exemplo, para testar o fluxo de dados de clientes de carga DFT no pacote LoadCustomers.dtsx, eu usei um parâmetro adicional do atributo UnitTest, chamado ExecutableName, para informar o mecanismo de teste que quero testar essa tarefa:

[UnitTest("CUSTOMERS", "LoadCustomers.dtsx",ExecutableName =
  @"\[LoadCustomers]\[SEQC Load]\[DFT Load customers]"))]
public class LoadCustomersTest : BaseUnitTest{
}

ExecutableName representa o caminho que combina nomes de Contêineres aninhados tudo começando com um nome de pacote.

Fluxo de controle e de dados para o LoadCustomers.dtsx são mostrados em Figura3.

Control Flow (Left) and Data Flow (Right) of the LoadCustomers.dtsx Package
Figura 3 controle fluxo (à esquerda) e fluxo de dados (direita) do pacote LoadCustomers.dtsx

Quando um teste alveja uma tarefa específica, somente essa tarefa é executada pelo mecanismo de teste. Se a execução bem sucedida da tarefa alvo depende a execução de tarefas anteriores, os resultados da execução dessas tarefas precisam ser gerado manualmente. O fluxo de dados de clientes de carga DFT espera ser truncado pela tarefa SQL CustomersStaging de truncar a tabela de destino. Além disso, o fluxo de dados espera que o arquivo de Customers.txt transformado em um local específico. Porque este arquivo é criado pelo pacote CopyCustomers.dtsx, preciso de copiá-lo manualmente. Aqui é o método de instalação que faz tudo isso:

protected override void Setup(SetupContext context){
  string dbConStr = @"Data Source=.;Integrated Security=SSPI;Initial Catalog=Demo";
  string ssisConStr = @"Provider=SQLNCLI11;" + dbConStr;
  File.Copy(@"\\nc1\Customers\Customers.txt", 
    @"C:\TestFiles\Converted\Customers.txt");
  context.DataAccess.OpenConnection(dbConStr);
  context.DataAccess.ExecuteNonQuery("truncate table [dbo].[CustomersStaging]");
  context.DataAccess.CloseConnection();
  DtsConnection conn = context.Package.GetConnection("CustomerDB");
  conn.SetConnectionString(ssisConnStr);
  conn = context.Package.GetConnection("CustomersSrc");
  conn.SetConnectionString(@"C:\TestFiles\Converted\Customers.txt");
}

Usando o File. Copy, eu copio o Customers.txt para o local esperado pelo fluxo de dados. Então eu uso a propriedade DataAccess do SetupContext para executar uma instrução truncate na tabela de destino. Essa propriedade expõe um wrapper ADO.NET leve que permite executar comandos SQL sem ter que usar classes SqlConnection e SqlCommand toda vez que você deseja acessar o banco de dados. No final, eu uso a pacote propriedade para definir as seqüências de conexão para os gerenciadores de conexões subjacentes.

Testes anteriores restrições

Também é possível escrever testes anteriores restrições de destino. Por exemplo, o CountConstraint que precede a tarefa de script do SCR CheckCount no pacote LoadCustomers.dtsx tem uma expressão que verifica se a variável CustomerCount é maior que zero. Se esta expressão for avaliada como true e a tarefa de carga SEQC é executado com êxito, em seguida, a tarefa de script é executada. Figura 4 mostra o teste de unidade completa.

Figura 4 o teste de unidade completa

[UnitTest("CUSTOMERS", "LoadCustomers.dtsx",
    PrecedenceConstraintsTestOnly = true))]
public class LoadCustomersConstraintsTest : BaseUnitTest{
  private DtsPrecedenceConstraint _countConstraint;
  protected override void Setup(SetupContext context){
    DtsVariable variable = context.Package.GetVariable("CustomerCount");
    variable.SetValue(0);
    _countConstraint =
      context.Package.GetPrecedingConstraintForPath(
      @"\[LoadCustomers]\[SCR    CheckCount].[CountConstraint]");
    _countConstraint.SetExecutionResult(DtsExecutionResult.Success);
  }
  protected override void Verify(VerificationContext context)
  {
    Assert.AreEqual(false, _countConstraint.Evaluate());
  }
  protected override void Teardown(TeardownContext context){}
}

Para preparar a restrição de precedência a ser testado, eu preciso fazer duas coisas. Primeiro, tenho que definir a variável CustomerCount para algum valor, porque a expressão na restrição de precedência refere-se a ele. Neste caso, escolho 0. Em seguida, defini o resultado da execução da tarefa anterior como sucesso, falha ou conclusão. Faço isso usando o método SetExecutionResult para simular o sucesso da tarefa anterior. Isso significa que CountConstraint deve ser avaliada como false, e isto é o que espero no método Verify. Você pode ter apenas uma classe onde você implementar testes de unidade para todas as restrições anteriores em um pacote. Portanto, não há nenhum caminho de destino para a restrição particular no atributo UnitTest, apenas uma bandeira de Bool que dirá ao mecanismo de que se trata de uma classe de teste de unidade para as restrições de precedência. A razão para isto é que, com as restrições de precedência, não há nenhuma necessidade para executar o pacote ou a tarefa antes que o método Verify é chamado.

Executando testes de unidade

Antes de executá-la meus testes, preciso carregar pacotes de alvo e testes em seus repositórios. Para fazer isso, preciso de uma referência para o mecanismo de teste. Abra o arquivo Program cs e substituir o método Main vazio com este:

static void Main{
  IUnitTestEngine engine = 
    EngineFactory.GetClassInstance<IUnitTestEngine>();
  engine.LoadPackages("CUSTOMERS", @"C:\TargetPackages\");
  engine.LoadUnitTests();
  engine.ExecuteUnitTestsWithGui();
}

A primeira linha cria uma referência para o mecanismo de teste. Para carregar todos os pacotes da pasta C:\TargetPackages\ para o repositório de clientes, uso o método de LoadPackages. O método LoadUnitTests carrega todas as classes no assembly de chamada que são decoradas com o atributo UnitTest no repositório do teste especificado. Finalmente, chamo ExecuteUnitTestsWithGui para iniciar a execução de testes e para abrir o GUI de monitoramento, que é mostrado no Figura5.

The Monitoring GUI During the Execution of Tests
Figura 5 o GUI acompanhamento durante a execução de testes

A GUI em Figura5 é prático se você quer testar localmente na sua máquina e você não quer começar Visual Studio. Se você gostaria de pacotes em um servidor de teste, você pode fazer pequenas modificações para o programa e agendá-lo para executar testes diretamente em um servidor de compilação, por exemplo:

static void Main{
  IUnitTestEngine engine = 
    EngineFactory.GetClassInstance<IUnitTestEngine>();
  engine.LoadPackages("CUSTOMERS", @"C:\TargetPackages\");
  engine.LoadUnitTests();
  engine.ExecuteUnitTests();
  engine.UnitTestResults.SaveAsHtml(@"C:\TestResults\");
}

A interface de IUnitTestEngine tem a propriedade de UnitTestResults que permite acessar o resultados de teste e salvá-los como um relatório em HTML. Troquei ExecuteUnitTestsWithGui com ExecuteUnitTests, que não mostra o monitoramento GUI. Você também pode executar testes dentro do Visual Studio ou uso ReSharper, assim você não precisa iniciar o programa de console. Para fazer isso, eu criei a nova classe chamada SSISUnitTestAdapter, mostrado na Figura 6.

Figura 6 a classe SSISUnitTestAdapter

[TestClass]
public class SSISUnitTestAdapter{
  IUnitTestEngine Engine {get;set;}
  [AssemblyInitialize]
  public static void Prepare(TestContext context){
    Engine = EngineFactory.GetClassInstance<IUnitTestEngine>();
    Engine.LoadPackages("CUSTOMERS", @"C:\TargetPackages\");
    Assembly testAssembly =
      Assembly.GetAssembly(typeof(CopyCustomersTest));
    Engine.LoadRepositoryUnitTests(testAssembly, "CUSTOMERS");
  }
  [TestMethod]
  public void CopyCustomersTest(){
    Engine.ExecuteUnitTest(typeof(CopyCustomersTest));
  }
  [TestMethod]
  public void LoadCustomersTest(){
    Engine.ExecuteUnitTest(typeof(LoadCustomersTest));
  }
  [TestMethod]
  public void LoadCustomersConstraintsTest(){
    Engine.ExecuteUnitTest(typeof(LoadCustomersConstraintsTest));
  }
}

Se você já trabalhou com a quadro antes de testes de unidade de Microsoft, você vai reconhecer os atributos de TestClass, AssemblyInitialize e Artigo13. Os três métodos de ensaio, CopyCustomersTest, LoadCustomersTest e LoadCustomersConstraintsTest, enrole a chamada do método ExecuteUnitTest, que por sua vez, executa os métodos de instalação, verifique se e subdivisão da classe que é passado como parâmetro. Prepare o método cria o objeto de teste do motor e carrega pacotes e unidade de testes em seus respectivos repositórios. Costumava ligeiramente diferentes métodos chamados LoadRepositoryUnitTests para carregar testes vinculados ao repositório clientes apenas. Isso é útil se você não quiser carregar todos os testes. Você pode executar todos os testes clicando em testes | Executar | Todos os testes no Visual Studio.

Criando testes de integração

A idéia básica de testes de unidade é isolar todos os possíveis efeitos de outros pacotes ou tarefas podem ter sobre o sendo testado. Às vezes isso pode ser um desafio para criar uma configuração de teste realistas e as condições iniciais necessárias para um teste de unidade garantir que o pacote ou a tarefa a ser testado se comporta como uma parte de um completo processo ETL. Porque você geralmente implementar processos ETL com um número de pacotes, você precisa realizar testes de integração para ter certeza que cada pacote funciona bem quando é executado como parte desse processo. A idéia é definir os pontos de sondagem em seu processo ETL onde você desejar para executar testes, sem ter que parar todo o processo. Como o processo progride e atinge o ponto de sondagem, os testes são executados e você pode verificar se um processo ETL "ao vivo" de trabalhos em andamento; daí o nome, "testar".

Um teste ao vivo é basicamente uma post-condição — definidos para um manipulador de eventos, a tarefa ou o pacote — que precisa ser satisfeito após o pacote, manipulador de evento ou tarefa foi executada. Este post-condition corresponde a etapa de verificação de um teste de unidade. Testes ao vivo são diferentes os testes de unidade, pois não é possível preparar o teste antes da execução do pacote ou para executar uma etapa de limpar depois. Isso ocorre porque ao contrário de um teste de unidade, um teste ao vivo não executa o pacote; é o contrário: Um pacote é executado um teste quando se trata de ponto de sondagem para o qual um post-condition definido.

Figura 7 ilustra esta diferença. Observe a posição do pacote em ambas as figuras. Quando os testes de unidade de execução, o mecanismo de teste explicitamente executa um teste de unidade, chamando seus métodos de instalação, verifique se e subdivisão. Um pacote é executado como uma parte desta seqüência de instalação-verificar-Teardown.

Sequence Diagrams for Unit Test (Left) and Live Test (Right) Execution
Figura 7 diagramas de sequência para a unidade de teste (à esquerda) e vivem (à direita) execução de teste

Por outro lado, quando executando testes ao vivo, o mecanismo de teste executa um pacote explicitamente, que por sua vez dispara a execução de métodos de ação que implementam as pós-condições para um pacote e suas tarefas.

A fim de criar um teste ao vivo para o pacote de CopyCustomers.dtsx, eu criei a nova classe chamada CopyCustomers, mostrado na Figura 8.

Figura 8 a classe CopyCustomers

[ActionClass("CUSTOMERS", "CopyCustomers.dtsx")]
public class CopyCustomers : BaseLiveTest{ 
  [ActionMethod(@"\[CopyCustomers]")]
  public void TestWholePackage(ActionContext context){
    Assert.AreEqual(true, context.Package.IsExecutionSuccess);
  }
  [ActionMethod(@"\[CopyCustomers]\[FST Copy Source File]")]
  public void TestCopySourceFile(ActionContext context){
    Assert.AreEqual(true, 
        context.ActiveExecutable.IsExecutionSuccess);
    Assert.AreEqual(true, 
        File.Exists(@"C:\TestFiles\Archive\Customers.txt"));
  }
  [ActionMethod(@"\[CopyCustomers]\[DFT Convert customer names]")]
  public void TestConvertCustomersNames(ActionContext context){
    Assert.AreEqual(true, context.ActiveExecutable.IsExecutionSuccess);
    string[] lines = 
        File.ReadAllLines(@"C:\TestFiles\Converted\Customers.txt");
    Assert.AreEqual("COMPANY2", lines[2].Split(‘,’)[1]);
  }
}

Cada classe de teste ao vivo deve derivar da classe BaseLiveTest, uma grande diferença quando comparado com um teste de unidade. A classe BaseLiveTest é usada internamente pelo mecanismo de teste para executar ensaios ao vivo e tem não métodos que têm de ser substituído. O atributo ActionClass marca essa classe como um teste ao vivo. Os parâmetros são os mesmos que quando usando o atributo UnitTest — repositório e destino do pacote. Observe que, ao contrário dos testes de unidade, onde cada teste é implementado em uma classe separada, única, apenas uma classe é necessária para implementar todas as pós-condições para um pacote. Classes de teste ao vivo podem ter um número arbitrário de pós-condições que devem ser avaliadas. Estes pós-condições correspondem ao método Verify em um teste de unidade e são implementadas como métodos decorados com o atributo ActionMethod. No exemplo em Figura 8, eu tenho um post-condition para cada tarefa no pacote e outro para o pacote em si. ActionMethod aceita um caminho para a tarefa de destino, que é o mesmo que o ExecutableName no atributo UnitTest. Isso informa o mecanismo de teste para executar este método quando executou a tarefa de destino. Ao contrário do método Verify, que sempre é executado, estes pós-condições não podem ser chamadas quando, por exemplo, a tarefa de destino não executa com êxito ou a restrição anterior for avaliada como false. O parâmetro ActionContext fornece a mesma funcionalidade que o VerificationContext.

Execução de testes ao vivo

As etapas necessárias para executar testes vivos são ligeiramente diferentes ao executar testes de unidade. Para executar testes vivos, substitua o método Main no arquivo Program. cs com o código em Figura 9.

Figura 9 o principal método para execução de testes ao vivo

static void Main{
  string dbConStr = @"Data Source=.;Integrated Security=SSPI;Initial Catalog=Demo";
  string ssisConStr = @"Provider=SQLNCLI11;" + dbConStr;
  ILiveTestEngine engine = 
    EngineFactory.GetClassInstance<ILiveTestEngine>();
  engine.LoadPackages("CUSTOMERS", @"C:\TargetPackages\");
  engine.LoadActions();
  ExecutionParameters params = new ExecutionParameters();
  params.AddVariable(@"\[Main].[ConnectionString]", ssisConStr);
  params.AddVariable(@"\[Main].[CopyCustomersPath]", 
    @"C:\TargetPackages\CopyCustomers.dtsx");
  params.AddVariable(@"\[Main].[LoadCustomersPath]", 
    @"C:\TargetPackages\LoadCustomers.dtsx");
  params.AddVariable(@"\[Main].[ConvertDestinationPath]",
    @"C:\TestFiles\Converted\Customers.txt");
  params.AddVariable(@"\[Main].[DestinationPath]", 
    @"C:\TestFiles\Archive\Customers.txt");
  params.AddVariable(@"\[Main].[SourcePath]", 
    @"\\nc1\Customers\Customers.txt");
  engine.SetExecutionParameters(parameters);
  engine.ExecuteLiveTestsWithGui("CUSTOMERS", "Main.dtsx");
}

Preciso de uma instância de ILiveTestEngine, que criei usando EngineFactory. Carregar pacotes é o mesmo que quando usando IUnitTestEngine. O método LoadActions carrega todas as ações definidas no assembly de chamada e é praticamente um equivalente de carga­UnitTests. Neste ponto, no entanto, a semelhança com a unidade de testes de paragens. Em vez de executar testes de unidade, digo-o mecanismo de teste para executar o pacote de Main.dtsx chamando o ExecuteLiveTestsWithGui.

Quando o pacote de Main.dtsx começa, corre o CopyCustomers.dtsx executando a tarefa de CopyCustomers de EPT. Cada um com sucesso terminado a tarefa nos gatilhos de CopyCustomers.dtsx um dos métodos de ação correspondente na classe CopyCustomersLiveTests. É importante notar que este teste testes implicitamente as definições de configuração do pacote CopyCustomers.dtsx.

Seus valores variáveis configuradas herdam o pacote Main.dtsx. Por favor, note que estas variáveis são usadas como seqüências de conexão nos gerenciadores de conexões de arquivos simples do pacote CopyCustomers.dtsx. Isso basicamente significa que o sucesso da execução das tarefas no pacote CopyCustomers.dtsx depende se a entrega de valor entre estes dois pacotes funciona corretamente. Este é um exemplo simples de como as interações e dependências entre pacotes são testadas, mas você pode imaginar cenários mais complexos, onde testes de unidade isolada não seriam suficiente para cobrir o caso de teste.

Teste motor Internals

A classe de núcleo que implementa as principais funções da biblioteca de SSISTester é TestEngine. É uma classe interna que é exposta através das interfaces IUnitTestEngine e ILiveTestEngine. Os dois métodos que revelam a maior parte da lógica interna são LoadUnitTests (mostrado no Figura 10) e ExecuteUnitTests.

Figura 10 o método LoadUnitTests

public void LoadUnitTests(){
  Assembly assembly = Assembly.GetCallingAssembly();
  IEnumerable<Type> types = assembly.GetTypes().Where(t => t.GetCustomAttributes(false).OfType<UnitTestAttribute>().Any() && 
    t.BaseType != null && t.BaseType.Name.Equals("BaseUnitTest"));
  foreach (Type t in types)
  {
    var attribute =
      t.GetCustomAttributes(false).OfType<UnitTestAttribute>().Single();
    DtsPackage package =
      _packages[attribute.Repository].GetForName(attribute.PackageName);
    string executable = attribute.ExecutableName;
    bool precedenceTestOnly = attribute.PrecedenceConstraintsTestOnly;
    var test = (BaseUnitTest)Activator.CreateInstance(t);
    test.TestClass = t;
    test.SetTestTargets(package, executable, precedenceTestOnly);
    test.Started += BaseUnitTestStarted;
    test.Finished += BaseUnitTestFinished;
    _unitTests.Add(test);
  }
}

LoadUnitTests basicamente itera todas as classes decoradas com o atributo UnitTest e cria uma instância para cada um. Essas instâncias são então convertidas em BaseUnitTest e são atribuídas o pacote de destino previamente carregado a partir do repositório de pacotes. No final, todas as instâncias são salvas na lista _unitTests. O método ExecuteUnitTests itera todas as instâncias de BaseUnitTest e chama o ExecuteTests em cada um:

public void ExecuteUnitTests(){
  foreach (BaseUnitTest t in _unitTests){
    t.ExecuteTest();
  }
}

A real execução de testes de unidade é implementada no método ExecuteTest (mostrado no Figura 11) na classe BaseUnitTest.

Figura 11 o método de ExecuteTest

public void ExecutTest(){
  Result = new UnitTestResult(Package, Executable) { TestOutcome =
    TestOutcome.InProgress, StartedAt = DateTime.Now };
  ExecuteSetup(CreateSetupContext());
  if (!Result.IsSetupSuccess)
    ExecuteTeardown(CreateTeardownContext());
  else{
    if(!PrecedenceOnly)
      Executable.Execute();
    ExecuteVerify(CreateVerifyContext());
    ExecuteTeardown(CreateTeardownContext());
    Result.FinishedAt = DateTime.Now;
  }
}

O aspecto mais importante deste método é que ele executa os métodos de instalação, verifique se e subdivisão, bem como o pacote de destino.

Conclusão

Os exemplos apresentados aqui, juntamente com o acompanhamento do projeto, devem permitir que você começar a testar seus projetos SSIS. Automatizar os testes de seus pacotes do SSIS pode te salvar um monte de tempo. O que é mais importante, automatizado de testes é mais confiável porque é feito continuamente e você pode cobrir mais pacotes. Uma vez que você tenha escrito testes, você sempre pode executá-los durante os processos de compilação automatizada. No final, isso significa menos erros e melhor qualidade.

Pavle Gudurić é um engenheiro de software localizado na Alemanha. Ele tem um mestrado em e-business e várias certificações técnicas e desenvolve soluções de business intelligence (BI) no setor de finanças. Contactá-lo no pavgud@gmail.com.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Christian Landgrebe (LPA) e Andrew Oakley (Microsoft)
Christian Landgrebe lidera a equipe de banco de dados no LPA, focada no fornecimento de soluções de BI para clientes na área financeira e de indústria de operação bancária.
Andrew Oakley é gerente sênior de programas sobre os padrões & equipe de práticas. Antes de se tornar um gerente de programa, Andrew passou dois anos como um evangelista técnico para o Visual Studio e a plataforma .NET. Seu projeto atual centra-se na orientação de acesso de dados em torno da construção de poliglota sistemas persistentes usando relacional e armazenamentos de dados NoSQL.