ti-enxame.com

Como uso o PHPUnit com o CodeIgniter?

Eu li e li artigos sobre PHPUnit, SimpleTest e outras estruturas de Teste de Unidade. Todos eles parecem tão bons! Finalmente consegui o PHPUnit trabalhando com o Codeigniter graças a https://bitbucket.org/kenjis/my-ciunit/overview

Agora, minha pergunta é: como eu o uso?

Todo tutorial que eu vejo tem algum uso abstrato como assertEquals(2, 1+1) ou:

public function testSpeakWithParams()
{
    $hello = new SayHello('Marco');
    $this->assertEquals("Hello Marco!", $hello->speak());
}

Isso é ótimo se eu tivesse uma função que produziria uma string tão previsível. Normalmente, meus aplicativos capturam um monte de dados do banco de dados e os exibem em algum tipo de tabela. Então, como eu testo os controladores do Codeigniter?

Eu gostaria de fazer o desenvolvimento orientado a testes e li o tutorial no site PHPUnits, mas mais uma vez o exemplo parece tão abstrato. A maioria das minhas funções de codeigniter estão exibindo dados.

Existe um livro ou um ótimo tutorial com uma aplicação prática e exemplos de testes do PHPUnit?

57
zechdc

Parece que você entende a estrutura/sintaxe básica de como escrever testes e testes de unidade. O código CodeIgniter não deve ser diferente do teste de código não CI, por isso quero focar em suas preocupações/problemas subjacentes ...

Eu tive perguntas semelhantes não muito tempo atrás com o PHPUnit. Como alguém sem treinamento formal, descobri que entrar na mentalidade dos Testes de Unidade parecia abstrato e antinatural a princípio. Eu acho que a principal razão para isso - no meu caso, e provavelmente o seu também da questão - é que você não se concentrou em [~ # ~] realmente [~ # ~] trabalhando para separar as preocupações em seu código até agora.

As asserções de teste parecem abstratas porque a maioria dos seus métodos/funções provavelmente executa várias tarefas distintas. Uma mentalidade de teste bem-sucedida exige uma mudança na maneira como você pensa sobre seu código. Você deve parar de definir o sucesso em termos de "isso funciona?" Em vez disso, você deve perguntar: "funciona, funcionará bem com outro código, foi projetado de uma maneira que o torna útil em outros aplicativos e posso verificar se funciona?"

Por exemplo, abaixo está um exemplo simplificado de como você provavelmente escreveu código até este ponto:

function parse_remote_page_txt($type = 'index')
{
  $remote_file = ConfigSingleton::$config_remote_site . "$type.php";
  $local_file  = ConfigSingleton::$config_save_path;

  if ($txt = file_get_contents($remote_file)) {
    if ($values_i_want_to_save = preg_match('//', $text)) {
      if (file_exists($local_file)) {
        $fh = fopen($local_file, 'w+');
        fwrite($fh, $values_i_want_to_save);
        fclose($fh);
        return TRUE;
      } else {
        return FALSE;
      }
  } else {
    return FALSE;
  }  
}

Exatamente o que está acontecendo aqui não é importante. Estou tentando ilustrar por que esse código é difícil de testar:

  • Está usando uma classe de configuração singleton para gerar valores. O sucesso da sua função depende dos valores do singleton e como você pode testar se essa função funciona corretamente em completo isolamento quando não é possível instanciar novos objetos de configuração com valores diferentes? Uma opção melhor pode ser passar sua função a $config argumento que consiste em um objeto de configuração ou matriz cujos valores você pode controlar. Isso é amplamente denominado " Injeção de Dependência " e há discussões sobre essa técnica em todas as interwebs.

  • Observe as instruções IF aninhadas. Testar significa que você está cobrindo todas as linhas executáveis ​​com algum tipo de teste. Ao aninhar instruções IF, você cria novas ramificações de código que requerem um novo caminho de teste.

  • Finalmente, você vê como essa função, embora pareça estar fazendo uma coisa (analisar o conteúdo de um arquivo remoto), na verdade, está executando várias tarefas? Se você zelosamente separa suas preocupações, seu código se torna infinitamente mais testável. Uma maneira muito mais testável de fazer a mesma coisa seria ...


class RemoteParser() {
  protected $local_path;
  protected $remote_path;
  protected $config;

  /**
   * Class constructor -- forces injection of $config object
   * @param ConfigObj $config
   */
  public function __construct(ConfigObj $config) {
    $this->config = $config;
  }

  /**
   * Setter for local_path property
   * @param string $filename
   */
  public function set_local_path($filename) {
    $file = filter_var($filename);
    $this->local_path = $this->config->local_path . "/$file.html";
  }

  /**
   * Setter for remote_path property
   * @param string $filename
   */
  public function set_remote_path($filename) {
    $file = filter_var($filename);
    $this->remote_path = $this->config->remote_site . "/$file.html";
  }

  /**
   * Retrieve the remote source
   * @return string Remote source text
   */
  public function get_remote_path_src() {
    if ( ! $this->remote_path) {
      throw new Exception("you didn't set the remote file yet!");
    }
    if ( ! $this->local_path) {
      throw new Exception("you didn't set the local file yet!");
    }
    if ( ! $remote_src = file_get_contents($this->remote_path)) {
      throw new Exception("we had a problem getting the remote file!");
    }

    return $remote_src;
  }

  /**
   * Parse a source string for the values we want
   * @param string $src
   * @return mixed Values array on success or bool(FALSE) on failure
   */
  public function parse_remote_src($src='') {
    $src = filter_validate($src);
    if (stristr($src, 'value_we_want_to_find')) {
      return array('val1', 'val2');
    } else {
      return FALSE;
    }
  }

  /**
   * Getter for remote file path property
   * @return string Remote path
   */
  public function get_remote_path() {
    return $this->remote_path;
  }

  /**
   * Getter for local file path property
   * @return string Local path
   */
  public function get_local_path() {
    return $this->local_path;
  }
}

Como você pode ver, cada um desses métodos de classe lida com uma função específica da classe que é facilmente testável. A recuperação remota de arquivos funcionou? Encontramos os valores que estávamos tentando analisar? Etc. De repente, essas afirmações abstratas parecem muito mais úteis.

IMHO, quanto mais você se dedica a testar, mais percebe que se trata mais de bom design de código e arquitetura sensível do que simplesmente garantir que as coisas funcionem conforme o esperado. E é aqui que os benefícios de OOP realmente começam a brilhar. Você pode testar o código procedural muito bem, mas para um projeto grande com teste de peças interdependentes é possível aplicar um bom design. Sei que pode ser troll isca para algumas pessoas processuais, mas tudo bem.

Quanto mais você testar, mais você se encontrará escrevendo código e se perguntando: "Posso testar isso?" E se não, você provavelmente mudará a estrutura naquele momento.

No entanto, o código não precisa ser elementar para ser testável. Stubbing and mocking permite testar operações externas cujo sucesso ou falha está totalmente fora de controle. Você pode criar fixtures para testar as operações do banco de dados e praticamente qualquer outra coisa.

Quanto mais eu testo, mais percebo que, se estou tendo dificuldades para testar algo, é mais provável que tenha um problema de design subjacente. Se eu resolver isso, geralmente resultará em todas as barras verdes nos resultados dos meus testes.

Finalmente, aqui estão alguns links que realmente me ajudaram a começar a pensar de uma maneira fácil de testar. O primeiro é ma lista direta do que NÃO fazer se você quiser escrever um código testável . De fato, se você navegar em todo o site, encontrará muitas coisas úteis que ajudarão a definir o caminho para 100% de cobertura do código. Outro artigo útil é este discussão sobre injeção de dependência .

Boa sorte!

95
rdlowrey

Tentei, sem êxito, usar o PHPUnit com o Codeigniter. Por exemplo, se eu quisesse testar meus modelos de IC, encontrei o problema de como obter uma instância desse modelo, pois de alguma forma ele precisa de toda a estrutura do IC para carregá-lo. Considere como você carrega um modelo, por exemplo:

$this->load->model("domain_model");

O problema é que, se você olhar a superclasse para um método de carregamento, não o encontrará. Não é tão simples se você estiver testando Plain Old PHP Objetos onde você pode zombar de suas dependências facilmente e testar a funcionalidade.

Portanto, decidi por classe de teste de unidade do CI .

my apps grab a bunch of data from the database then display it in some sort of table.

Se você está testando seus controladores, está essencialmente testando a lógica de negócios (se houver) lá, bem como a consulta sql que "captura um monte de dados" do banco de dados. Isso já é teste de integração.

A melhor maneira é testar o modelo do IC primeiro para testar a captura de dados - isso será útil se você tiver uma consulta muito complicada - e depois o controlador ao lado para testar a lógica de negócios aplicada aos dados capturados. pelo modelo de IC. É uma boa prática testar apenas uma coisa de cada vez. Então, o que você vai testar? A consulta ou a lógica de negócios?

Estou assumindo que você deseja testar a captura de dados primeiro, as etapas gerais são

  1. Obtenha alguns dados de teste e configure seu banco de dados, tabelas etc.

  2. Tenha algum mecanismo para preencher o banco de dados com dados de teste e excluí-lo após o teste. Extensão de banco de dados do PHPUnit tem uma maneira de fazer isso, embora eu não saiba se isso é suportado pela estrutura que você postou. Nos informe.

  3. Escreva seu teste, passe-o.

Seu método de teste pode ficar assim:

// At this point database has already been populated
public function testGetSomethingFromDB() {
    $something_model = $this->load->model("domain_model");
    $results = $something_model->getSomethings();
    $this->assertEquals(array(
       "item1","item2"), $results);

}
// After test is run database is truncated. 

Caso você queira usar a classe de teste de unidade do CI, aqui está um trecho de código modificado de um teste que escrevi usando:

class User extends CI_Controller {
    function __construct() {
        parent::__construct(false);
        $this->load->model("user_model");
        $this->load->library("unit_test");
    }

public function testGetZone() {
            // POPULATE DATA FIRST
    $user1 = array(
        'user_no' => 11,
        'first_name' => 'First',
        'last_name' => 'User'
    );

    $this->db->insert('user',$user1);

            // run method
    $all = $this->user_model->get_all_users();
            // and test
    echo $this->unit->run(count($all),1);

            // DELETE
    $this->db->delete('user',array('user_no' => 11));

}
2
Jeune