English Русский 中文 Español Deutsch 日本語
Melhorar a Qualidade do Código com Ajuda do Teste de Unidade

Melhorar a Qualidade do Código com Ajuda do Teste de Unidade

MetaTrader 4Exemplos | 26 outubro 2015, 12:55
814 0
Андрей
Андрей

Introdução

Como programador em MQL4, eu tenho uma vasta experiência lidando com alguns programas MQL4 prontos e criando várias dezenas de meus próprios programas. Como resultado, cheguei à conclusão que MQL4 é um ambiente muito favorável para a criação de programas de baixa qualidade. As razões estão listadas abaixo:

  1. MetaTrader 4 não tem nenhum depurador embutido. Pesquisar por erros pode ser um processo bastante problemático.
  2. MQL4 não tem facilidades como no tratamento de exceções em C++ ou Java.
  3. Programas MQL4 são muitas vezes criados numa corrida mais compromissada com uma idéia do que com a qualidade do código.

Estes fatos por sua vez resultam nas seguintes questões:

  1. Erros na operação dos advisors, erro do algoritmo da operação (especialmente erros críticos nas contas reais).
  2. Execução lenta. Otimização muito lenta.
  3. Mau tratamento dos casos de erro. Um expert advisor tornar-se não operável.

Eu gostaria de deixar a observação que o contexto acima não concerne aos programadores experientes de longas datas em MQL4. Programadores habilidosos encontram maneiras de criar um código com alta qualidade.

O meu trabalho principal é relacionado a testes de qualidade de software, então fiquei interessado em quaisquer materiais relativos a testes e depuração de programas MQL4, mas o número de artigos encontrados que são relativos a esta questão foram muito poucos. Portanto, eu gostaria de descrever aqui uma das formas de melhorar a qualidade dos programas. Caso este tema revele-se interessante, outras questões podem ser abordadas em próximos artigos.


Algumas Teorias Relativas à Qualidade

Se nós pesquisarmos no google um pouquinho, podemos descobrir que:

Qualidade é um complexo de propriedades de produção e características que dá a esta possibilidade de produção a satisfação das necessidades condicionais ou assumidas.

Tanto quanto o software está em sintonia com o seu objetivo, podemos considerar que o programa tem uma boa qualidade e está em conformidade com as necessidades do cliente, exercendo as funções que lhe são concernentes corretamente.

Dois tipos de atividades geralmente são necessárias para fazer um programa de boa qualidade:

  • Garantia de Qualidade - medidas que são tomadas para evitar os defeitos.
  • Controle de Qualidade - Controle de um programa finalizado com qualidade voltada à detecção de defeitos, no caso da Garantia de Qualidade não ajudar. É necessário entender que o defeito de um código dificilmente será eliminado se não for detectado e claro que é especialmente ruim quando um defeito é detectado pelo seu cliente...

A garantia de qualidade é uma questão complicada. Tratam-se de muitas tarefas que começam a partir de um local de trabalho confortável do programador para a criação até a implementação de processos de negócios complexos. Nós não vamos tocar neste problema ainda. Vamos falar de Controle de Qualidade.

Quanto mais atenção dermos ao controle de qualidade, maior é a chance do nosso programa funcionar como um encanto. Teoricamente o controle de qualidade (ou teste, em outras palavras) deve ser realizado em todas as fases de desenvolvimento:

  1. Testes de especificação técnica - é impossível desenvolver um programa funcionando normalmente com base numa especificação técnica incorreta.
  2. Fonte de revisão do código. Procurar falhas, código ineficaz, violação de regras da codificação, erros evidentes.
  3. Testes de funções distintas do programa no modo automático (Teste de Unidade).
  4. Testar o programa finalizado no modo manual. Um ser humano (analista de teste) verifica se o programa está funcionando corretamente.
  5. Teste do programa no modo automático, ou seja, quando os robôs testam a qualidade do programa por conta própria. Parece uma utopia, mas às vezes funciona.
  6. Teste do programa pelo cliente.

etc. Existem um monte de tipo de testes...

E o Teste de Unidade é um dos mais interessantes.


Algumas Teoria Preocupantes dos Testes de Unidades

O Google nos dá a seguinte definição: Teste de Unidade - é um método para a aplicação de validação no qual um programador verifica unidades distintas (blocos) de um código-fonte para a sua utilização no resto do programa. Unidade é a menor parte de um programa que é adequado para testes. Em linguagens de aplicação (incluindo MQL4) uma função distinta pode ser considerado como uma unidade.

Na maioria dos casos o Teste de Unidade é feito automaticamente. Em outras palavras, é criado um programa que chama uma função a ser examinada com diferentes parâmetros, em seguida o programa cria um relatório indicando se os valores trazidas pela função são verdadeiros ou não.

Os Testes de Unidade podem ser extremamente úteis pelas seguintes razões:

  1. No caso de uma falha, você pode encontrar suas raízes facilmente, pois uma única função foi testada. No caso de uma falha em todo o aplicativo, você terá que gastar algum tempo adicional para encontrar a função responsável pelo problema.
  2. É bastante fácil de verificar se um defeito é eliminado ou não, basta executar o Teste de Unidade automático mais uma vez, não há necessidade de reiniciar o aplicativo inteiro. Por exemplo, pode haver alguns erros aparecendo em algumas ocasiões raras que dificilmente podem ser reconstituídos, os Testes de Unidades eliminam este problema.
  3. Você pode otimizar o código da função facilmente sem quaisquer preocupações de que algo vai dar errado. Teste de Unidade pode mostrar sempre se uma função continua trabalhando normalmente ou não.
  4. Também você pode detectar problemas que não se manifestam uma única vez, mas podem aparecer na casa do cliente e requerem grandes quantidades de horas de busca e depuração.
  5. Uma abordagem moderna de Teste de Impulsão pode ser usado, ou seja, quando um Teste de Unidade é criado no início e em seguida a função é desenvolvida, a função é desenvolvida até o teste de unidade ser transmitido. Eu tentei esta abordagem num aplicativos C++ e provou ser boa, senti um prazer genuíno, pois fiquei totalmente confiante na usabilidade das funções no final do desenvolvimento e a posterior utilização no programa foi impecável.

Vamos ver como se parece. Suponha que nós estamos desenvolvendo uma função com raiz quadrada:
y=sqrt(x)

Vamos criar uma outra função para o teste que funcionará de acordo com o seguinte algoritmo:

  • verifica que sqrt(-1) == error
  • verifica que sqrt(0) == 0
  • verifica que sqrt(0.01) == 0.1
  • verifica que sqrt(1) == 1
  • verifica que sqrt(4) == 2
  • verifica que sqrt(7) == 2.6....


Podemos criar uma função de teste antes desenvolver a principal, para isto vamos determinar os requisitos que função desenvolvida deve cumprir. Esta é a nossa utilização da abordagem do Teste de Impulsão, e poderemos usar a função no nosso programa principal seguramente somente após o nosso Teste de Unidade mostrar um trabalho impecável.

Mas uma questão permanece em aberto: como devemos selecionar um conjunto de parâmetros a testar para uma função testada? Naturalmente é necessário utilizar todos os valores possíveis, mas em quase todos os casos é impossível ou muito trabalhoso. Um novo artigo poderia ser escrito sobre a seleção dos valores para testes. Aqui eu vou tentar dar algumas dicas gerais:

  1. É necessário a utilização dos dados corretos e dos dados que levam aos erros, pois temos de verificar não apenas se a função cumpre as finalidades, mas também o quanto será boa no processamento dos erros.
  2. Precisamos usar valores de limite, por exemplo, se o intervalo dos valores é de 0 até 100, os valores que são iguais a 0 e 100 devem ser utilizados. Se os dados de entrada consistem em linhas, devem ser experimentadas linha vazias e com comprimento máximo.
  3. Os valores que vão além das marcas permitidas devem ser utilizados. Se olharmos para o exemplo anterior proposto, os valores referente a 101 e -1 devem ser usados, o valor máximo de + 1 deve ser utilizado para a linha.
  4. Devemos tentar quebrar a multidão de todos os valores possíveis para os subconjuntos das equivalências (classes de equivalência) para qual caminho de comportamento da função é semelhante. Um valor deve ser selecionado para cada classe, por exemplo, não há nenhum ponto a verificar na sqrt(5) e sqrt(9). É muito mais interessante verificar sqrt (5) e sqrt (9) tal como no último caso, pois a função irá trazer o valor irracional de volta, enquanto que no primeiro caso será um integral.
  5. No caso, a função tem ramos (if,switch), devemos assegurar que cada um deles é processado pelo Teste da Unidade.

Vou tentar mostrar isso no próximo capítulo usando um exemplo definitivo.


Algumas Práticas Concernentes ao Desenvolvimento de Teste de Unidade

Vamos definir uma meta de treinamento! Suponhamos, nossa tarefa é desenvolver uma biblioteca que tem a função de aceitar dois arrays na entrada. A função exclui os elementos que estão ausentes no segundo a partir do primeiro array, como resultado, o primeira array é um subconjunto da segundo.

Vamos determinar o protótipo da nossa função:

void CreateSubset(int & a1[], int a2[]);

Vamos tentar utilizar a abordagem do Teste de Impulsão no desenvolvimento da função. Vamos determinar um conjunto de dados do teste. Devemos marcar várias classes de equivalência de dados de entrada para alcançar este objetivo:

  1. Ambos os arrays estão vazios.
  2. A1 é vazio, A2 contém os elementos.
  3. A1 contém os elementos, A2 está vazio.
  4. Ambos contêm conjunto semelhante de elementos e têm tamanho similar.
  5. A1 contém os elementos que não estão presentes em A2.
  6. Parte dos elementos em A1 estão presentes em A2, parte de A2 está contido em A1 (ambos conjuntos têm uma intersecção).
  7. Uma pequena parte dos elementos A1 está presente em A2
  8. Uma pequena parte dos elementos A1 está presente em A2. Além disso, os elementos estão espalhados por todo o array.
  9. Uma pequena parte dos elementos A1 está presente em A2. Além disso, os elementos estão concentrados no início do array.
  10. Uma pequena parte dos elementos A1 está presente em A2. Além disso, os elementos estão concentrados no final do array.

No caso de nossa função trabalhar bem para todos os 10 casos, podemos ter certeza absoluta de que os experts que usam esta função não serão vítimas da imperfeição dela, mas devemos entender que é impossível testar 100% e alguns possíveis defeitos latentes pode permanecer sempre.

Eu criei uma pequena biblioteca mql4unit por conveniência. Eu incluí nesta biblioteca as funções que são necessárias aos testes de unidade:

//-------------------------------------------------------------------+

// Condições de teste atuais são mantidas pelas variáveis globais
//-------------------------------------------------------------------+
int tests_passed;    //Número de testes bem sucedidos
int tests_failed;    //Número de testes malsucedidos
int tests_total;     //Número total de testes

string test_name;    //Nome do Teste

//-------------------------------------------------------------------+
//A função inicializa o ambiente de teste para um teste
//-------------------------------------------------------------------+
void UnitTestStart(string TestName)
{

   test_name = TestName;
   tests_passed = 0;
   tests_failed = 0;
   tests_total = 0;
   Print("*--------------------------------------------------*");

   Print("Starting unit test execution ", test_name);
}

//-------------------------------------------------------------------+
//a função é chamada no final do teste. Retorna verdadeiro se todos os testes
//são bem sucedidos. Caso contrário - Falso.
//-------------------------------------------------------------------+
bool UnitTestEnd()
{
   if (tests_failed == 0)

   {
      Print("HURRAY!!! ", test_name, " PASSED. ", tests_passed, " tests are successful.");
   }
   else
   {

      Print(":((( ", test_name, " FAILED. ", tests_passed,"/",tests_total, " tests are successful.");   
   }
   Print("*--------------------------------------------------*");
}


//-------------------------------------------------------------------+
//A função executa o teste por dois arrays do tipo int
//Retorna verdadeiro, se os arrays são iguais
//-------------------------------------------------------------------+
bool TestIntArray(int actual[], int expected[]){

   tests_total++;
   //Comparando o tamanho dos arrays
   if (ArraySize(actual) != ArraySize(expected))
   {
      Print("Test #", tests_total," ERROR. Array size ", ArraySize(actual), " instead of ", ArraySize(expected));

      tests_failed++;
      return(false);      
   }
   //Comparando elemento por elemento
   for (int i=0; i<ArraySize(actual);i++)

   {
      if (actual[i]!=expected[i]){
         Print("Test #", tests_total," ERROR. Element value #",i,"=", actual, " instead of ", expected);
         tests_failed++;

         return(false);
      }
   }
   //Se todos os elementos são iguais, o teste é transmitido
   Print("Test #", tests_total," OK: Passed!");  

   tests_passed++;
   return(true);
}

Vamos criar script de teste "mytests" com um corpo vazio de nossa função. Criar função de teste e descrever todos os testes de unidade nele.
bool Test()
{
   UnitTestStart("CreateSubset function testing");
   Print("1. Both arrays are empty.");

   int a1_1[], a1_2[];
   int result_1[]; //Esperando por um array vazio como resultado da execução da função
   CreateSubset(a1_1, a1_2);
   TestIntArray(a1_1, result_1);
   
   Print("2. A1 is empty, A2 contains the elements");

   int a2_1[], a2_2[] = {1,2,3};
   int result_2[]; //Esperando por um array vazio como resultado da execução da função
   CreateSubset(a2_1, a2_2);

   TestIntArray(a2_1, result_2);

   Print("3. A1 contains the elements, A2 is empty");
   int a3_1[] = {1,2,3}, a3_2[];

   int result_3[]; //Esperando por um array vazio como resultado da execução da função
   CreateSubset(a3_1, a3_2);
   TestIntArray(a3_1, result_3);

   Print("4. Both contain similar set of the elements and have similar size");
   int a4_1[] = {1,2,3}, a4_2[] = {1,2,3};

   int result_4[] = {1,2,3}; //À espera de um array inalterado como resultado da execução da função
   CreateSubset(a4_1, a4_2);
   TestIntArray(a4_1, result_4);

   Print("5. A1 contains the elements that are not present in A2");

   int a5_1[] = {4,5,6}, a5_2[] = {1,2,3};
   int result_5[]; //Esperando por um array vazio como resultado da execução da função

   CreateSubset(a5_1, a5_2);
   TestIntArray(a5_1, result_5);
   
   Print("6. Part of the elements in A1 are present in A2, A2 part is contained in A1 (both multitudes have an intersection)");
   int a6_1[] = {1,2,3,4,5,6,7,8,9,10}, a6_2[] = {3,5,7,9,11,13,15};

   int result_6[] = {3,5,7,9}; //Esperando por arrays de intersecção como resultado da execução da função
   CreateSubset(a6_1, a6_2);
   TestIntArray(a6_1, result_6);

   
   Print("7. All A1 elements are present in A2, but A2 size is bigger");
   int a7_1[] = {3,4,5}, a7_2[] = {1,2,3,4,5,6,7,8,9,10};

   int result_7[] = {3,4,5}; //Esperando por arrays de intersecção como resultado da execução da função
   CreateSubset(a7_1, a7_2);
   TestIntArray(a7_1, result_7);
   

   Print("8. A small part of A1 elements is present in A2. Besides, the elements are scattered all over an array.");
   int a8_1[] = {1,2,3,4,5,6,7,8,9,10}, a8_2[] = {2,5,9};

   int result_8[] = {2,5,9}; //Esperando por arrays de intersecção, como resultado da execução da função
   CreateSubset(a8_1, a8_2);
   TestIntArray(a8_1, result_8);
   

   Print("9. A small part of A1 elements is present in A2. Besides, the elements are concentrated at an array leader.");
   int a9_1[] = {1,2,3,4,5,6,7,8,9,10}, a9_2[] = {1,2,3};

   int result_9[] = {1,2,3}; //Esperando por arrays de intersecção, como resultado da execução da função
   CreateSubset(a9_1, a9_2);
   TestIntArray(a9_1, result_9);

   Print("10. A small part of A1 elements is present in A2. Besides, the elements are concentrated at an array's end.");

   int a10_1[] = {1,2,3,4,5,6,7,8,9,10}, a10_2[] = {8,9,10};

   int result_10[] = {8,9,10}; //Esperando por arrays de intersecção, como resultado da execução da função
   CreateSubset(a10_1, a10_2);
   TestIntArray(a10_1, result_10);
   

   return (UnitTestEnd());
}

Para executar o Teste de Unidade temos que chamar a função Test na função principal e executar o script.

Vamos executar o nosso teste.


Como podemos ver, os resultados são decepcionantes, isto não surpreende já que a função não está pronto em tudo, mas nem tudo está perdido! 4 testes em cada 10 são passados com êxito. Teoricamente isso significa que nós poderíamos ter esquecido o fato que a função está vazia em alguns casos e teria atuado normalmente.

Em outras palavras, pode haver um tal subconjunto de dados de entrada para que uma função errada funcione corretamente. Se um programador tivesse aplicado apenas como os dados de teste para alcançar o sucesso, a função vazia poderia facilmente ter ido para um cliente.

Agora, vamos criar a própria função CreateSubset. Não vamos discutir a eficiência e a beleza desta função aqui.

void CreateSubset(int & a1[], int a2[]){
   int i=0;

   while(i<ArraySize(a1)){
      bool b_exist = false;
      for (int j=0; j<ArraySize(a2);j++){

         if (a1[i] == a2[j]) b_exist = true;
      }
      if (!b_exist){
         for (j=i; j<ArraySize(a1)-1;j++){
            a1[j] = a1[j+1];   

         }
         ArrayResize(a1, ArraySize(a1)-1);
      }else{
         i++;
      }
   }
}
Vamos executar o teste novamente:


A função pode ser executado a partir de qualquer lugar. Pode ser determinada dentro de um expert e executado durante a inicialização. No caso de processar um módulo separado, uma ou várias funções de teste podem ser determinadas dentro dela e chamadas a partir de um script. Aqui podemos fantasiar um pouco.

Claro que a variante ideal seria a possibilidade de executar um teste de unidade direto, depois da compilação da biblioteca, mas ainda não sabemos se isso pode ser feito em MQL4. Muito provavelmente não. Se você sabe como fazer, por favor escreva-me!

Toda vez que executamos o teste, podemos suspirar de alívio e ter certeza de tudo funcionará como deveria.


Alguns Comentários

  1. Pode parecer que escrever testes consome tempo, Mas eu lhe garanto que a dedicação ao desenvolvimento de testes de unidade vai valer a pena e você vai ganhar tempo na realidade.
  2. Claro que não vale a pena desenvolver testes de unidade para todas as funções. O balanço deve ser mantido entre importância, probabilidade de falha e o valor da função dentro código. Por exemplo, não há nenhuma necessidade de se escrever um ensaio para umaa função de simples consistindo de poucas linhas.
  3. Você pode fazer qualquer coisa que você gosta dentro do Teste de Unidade: abrir/fechar ordens, usar indicadores, objetos gráficos etc. Suas ações não se limitam aqui.


Finalmente, uma última coisa

Espero que este material seja útil para você, terei prazer em responder a todas suas perguntas. Também estou aberto a todas as sugestões sobre as possíveis formas de melhorar este artigo e escrever novos.

Eu desejo a você toda boa sorte e códigos irrepreensíveis!

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/1579

Arquivos anexados |
mql4unit.mq4 (5.31 KB)
testscript.mq4 (4.34 KB)
Ação do Preço: Automatizando a Estratégia de Negociação "Inside Bar" Ação do Preço: Automatizando a Estratégia de Negociação "Inside Bar"
O artigo descreve o desenvolvimento de um Expert Advisor MetaTrader 4 baseado na estratégia "Inside Bar" (Barra Envolvida), incluindo princípios para detecção deste padrão, bem como configurações de regras para uma ordem stop e pendente. Os resultados dos testes e otimização são publicados também.
Concurso de Expert Advisors dentro de um Expert Advisor Concurso de Expert Advisors dentro de um Expert Advisor
Usando negociação virtual, você pode criar um Expert Advisor adaptativo que vai ligar e desligar as negociações no mercado real. Combine várias estratégias num único Expert Advisor! O sistema múltiplo de Expert Advisor irá escolher automaticamente uma estratégia de negociação, aquela mais apropriada ao mercado real com base na rentabilidade dos negócios virtuais. Este tipo de abordagem permite diminuir o rebaixamento e aumentar a rentabilidade do seu investimento no mercado. Experimente e compartilhe seus resultados com os outros! Eu acho que muitas pessoas vão se interessar em saber sobre o seu portfólio de estratégias.
Expert Advisor para Negociação no Canal Expert Advisor para Negociação no Canal
O Expert Advisor desenha linhas para formar um canal. As linhas de canal superior e inferior atuam como níveis de suporte e resistência. O Expert Advisor marca pontos de referência, fornece notificação sonora toda vez que o preço atinge ou cruza as linhas do canal e desenha os símbolos dos pontos principais. Após a formação do fractal, as setas correspondentes aparecem nas últimas barras. Linhas de rompimentos podem sugerir a possibilidade de uma tendência crescente. O Expert Advisor é amplamente comentado em toda a sua extensão.
A Mágica dos Filtros A Mágica dos Filtros
A maioria dos desenvolvedores de sistemas automatizados de negociação usam algum tipo de filtro nos sinais de negociação. Neste artigo, vamos explorar a criação e implementação de filtros passa-faixa e discretos e de cruzamento de bandas para Expert Advisors, com o objetivo de melhorar as características do sistema de negociação automática.