O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

Qualidade e Teste de Código - RGSS Avançado

Iniciado por Resque, 28/01/2017 às 18:34

28/01/2017 às 18:34 Última edição: 31/01/2017 às 22:35 por Resque
Qualidade e Teste de Código - RGSS Avançado

[box2 class=titlebg title=◊ Introdução]Estarei apresentando abaixo alguns conceitos sobre qualidade de código em jogos e sistemas em gerais, tentarei não me prender muito no RGSS, para que fique mais fácil de ser absorvido por quem utiliza Javascript (MV).

  Assim como informado no título do tópico, tratarei de temas avançado da linguagem Ruby e por esse motivo, partirei do princípio de que você já tenha um sólido conhecimento na linguagem, assim como controle de fluxo, loops e orientação a objetos.

  Não estarei explicando funcionalidades básicas da linguagem, focando no conceito de qualidade para que qualquer pessoa consiga aplicar os mesmos em outras linguagens.

  É de suma importância que você saiba ou tenha conceito de como trabalhar com Blocos (Blocks), uma das ferramentas mais poderosa que a linguagem Ruby possui, caso não tenha conhecimento, sugiro que leia esse tutorial: http://guru-sp.github.io/tutorial_ruby/blocos-ruby.html
[/box2]

[box2 class=titlebg title=◊ O que é erro?]
  Segundo o site www.dicionarioinformal.com.br, a definição de erro é:

    - Consequência de uma ação inesperada, sem planejamento, conhecimento. Pode ser uma falha humana ou por equipamento.


  Na maioria das vezes, o erro ocorre por falta de conhecimento, pois se o erro fosse descoberto antes, não ocorreria, isso pode ocorrer em coisas planejadas ou não.

  O segredo está no momento que o erro é descoberto, quando mais tarde, pior.

  No ramo da Engenharia de Sistemas, consideramos bastante a regra 10 de Myers, ela nos informa que, quando mais tarde o erro for descoberto, maior será o custo para que ele seja corrigido.

[/box2]

[box2 class=titlebg title=◊ Qual é o impacto dos erros?]
  Como visto no gráfico de Myers, um erro descoberto na fase inicial do seu projeto será bem mais barato de se corrigir do que se fosse encontrado após a fase final.

  O acidente ocorrido no dia 1 de setembro de 2016, em que  a Space X (empresa de transporte espacial) estava se preparando para realizar um teste estático em seu foguete Falcon 9, esse teste é sempre utilizado antes dos lançamento de foguetes para verificar se está tudo certo com os motores.

  O teste foi realizado enquanto o foguete Falcon 9 estava com o satélite AMOS-6 em seu topo, um satélite de comunicação geoestacionário israelense da Spacecom de 5,5 toneladas e avaliado em US$ 285 milhões.

  Resultado?

 
Spoiler

 
 
[close]

  O satélite estava coberto por um seguro, porém o seguro só pagaria se o foguete fosse mandado para o espaço (não literalmente).

 
Spoiler

 
 
[close]

  A causa do acidente ainda é desconhecida.

  Vamos trazer esse acidente para o mundo do Software e colocar esse erro no gráfico de Myers, a Space X poderia ter evitado o prejuízo de US$ 285 milhões e a imagem da empresa se tivesse descoberto a falha no início do projeto, talvez foi apenas um parafuso que o estagiário esqueceu de apertar, ou algo que no máximo atrasaria o lançamento do foguete com o satélite.
[/box2]

[box2 class=titlebg title=◊ Como evitar]
  Durante a fase de desenvolvimento do projeto, novas funcionalidades são inseridas e as existentes são alteras.

  É muito fácil acontecer incompatibilidades e caminhos não previstos, essa fase é a responsável por gerar mais erros, porém é onde a correção deve ser feita em paralelo.

  O problema é que, quanto maior fica o nosso sistema, mais difícil será de testar todos os caminhos e todas a funcionalidades cada vez q o sistema for alterado.

    - Novas funcionalidades geram erro.
    - Alterações geram erro.
    - Correções de erros geram erros.

  Testar todo o sistema manualmente, se torna cada vez mais cansativo, demorado e ineficiente.

  Precisamos de alguém que consiga resolver problemas de forma mais rápida do que nós.

  Você consegue resolver o problema matemático abaixo em menos de um segundo?

    1293495 - 29348 / 6 + 7 * 12

  Eu também não, mas o computador consegue.


  Existem dois tipos de inteligência artificial, a fraca e a forte.

    - A fraca está avançando mundialmente com uma velocidade incrível, ela é responsável por resolver problemas específicos, como: Reconhecimento de voz, tradução de texto, corretor ortográfico (MS Word, Skype, Navegadores de Internet), etc..

    - A forte avança lentamente, ainda está no tempo das cavernas, quebrando pedra e matando dinossauros, seria algo como essa inteligência escrever esse tutorial no meu lugar sem qualquer ajuda.

  Já que o computador consegue nos ajudar a resolver problemas específicos e de forma rápida, por quê não utiliza-lo para fazer os nossos testes?
[/box2]

[box2 class=titlebg title=◊ Testes Automatizados]
  No mundo do Software, essa técnica é fundamental garantir a qualidade de um sistema.

  Os benefícios de testes automatizados são:

  - Testes mais rápidos;
  - Segurança de alterações;
  - Especificações descritas no código;
  - Abertura de possibilidades de refatoração;
  - Prevenção e redução de bugs.
  - Melhoria no design de classes

  Agora você já deve estar concordando comigo sobre o impacto de erros em um programa e também deve estar se lembrando da edição que você fez no seu script e que nem chegou a testar (sim, tem um erro lá).

  Chegamos na parte prática do nosso tutorial, e vamos criar um pequeno script para o nosso jogo.

  Utilizaremos o RPG Maker VX Ace com RGSS3 e scripts auxiliares.
[/box2]

[box2 class=titlebg title=◊ Scripts Everywhere]
  Para esse tutorial, criaremos um simples script que exibe o nome do personagem mapa, bem abaixo do personagem, algo bem comum em jogos online.

  Primeiro vamos criar uma classe chamada Resque_Character_Name, ela terá toda a regra para a exibição do nome do personagem.

  Crie um novo item na sua aba de Scripts, bem abaixo  de '▼ Materials', e coloque o código:

 
  class Resque_Character_Name
  end
 


  Agora que criamos a classe, precisamos criar o teste para essa classe, para isso, eu criei um script que nos ajudará com o teste.
[/box2]

[box2 class=titlebg title=◊ Rtest]
  Este script, disponibiliza alguns métodos necessários para que a classe de produção (vamos chamar assim a classe que contem toda a nossa lógica) seja testada com eficiência.

  O nome Rtest foi escolhido por  conta d̶o̶ ̶m̶e̶u̶ ̶n̶i̶c̶k̶  de ser um script de testes para RGSS3.

  O Rtest disponibiliza para você, os métodos abaixo:

  .antes
    Recebe um bloco como parâmetro e executa o conteúdo. É utilizado para a definição de variáveis que serão utilizadas dentro dos testes.

    Ex:

   
      antes do
        @heroi = Hero.new
      end
   


  .afirmar
    Recebe um parâmetro (verdadeiro ou falso). É utilizado para verificar se um valor é verdadeiro (true).

    Ex:
   
    afirmar @heroi.vivo?
   


    Obs: Caso o parâmetro for falso, será lançado uma mensagem no console do RPG Maker.
    Em caso de sucesso, irá lançar um "." ponto, para informar que o teste passou com sucesso.

  .nao_afirmar
    A forma negativa do "afirmar", Recebe um parâmetro (verdadeiro ou falso). É utilizado para verificar se o valor é falso.

   
    nao_afirmar @heroi.andando?
   

    Obs: Se o parâmetro for verdadeiro, será lançado uma mensagem no console do RPG Maker.
         Em caso de sucesso, irá lançar um "." ponto, para informar que o teste passou com sucesso.

  .afirmar_igualdade
     Recebe dois parâmetros e compara se são iguais. O primeiro parâmetro é a função da classe testada, e o segundo é o que esperamos dela.

    Ex:
   
    nome = Resque

    afirmar_igualdade nome, 'Resque'
    # Resultado: OK

    afirmar_igualdade nome, 'Thiago'
    # Alerta de erro na comparação
   


    Obs: Caso o retorno da função seja diferente do esperado, será lançado uma mensagem no console do RPG Maker.
         Em caso de sucesso, irá lançar um "." ponto, para informar que o teste passou com sucesso.

  .afirmar_desigualdade
     A forma negativa do "afirmar_igualdade".

    Ex:
   
    nome = "xxxx"
    afirmar_desigualdade nome, "yyyy"
   


    Obs: Caso o retorno da função seja igual ao esperado, será lançado uma mensagem no console do RPG Maker.
         Em caso de sucesso, irá lançar um "." ponto, para informar que o teste passou com sucesso.

  .isso
    Método utilizado para descrever o teste, recebe o nome do teste como primeiro parâmetro e um bloco com as verificações utilizando os métodos acima.

    EX:
   
    isso 'deve ser um pato' do
      afirmar_igualdade animal.tipo, Pato
    end
   


  Basicamente o script de testes, nos fornece vários métodos que ajudam a verificar se a classe de produção está devolvendo os valores que esperamos.
[/box2]

[box2 class=titlebg title=◊ Criando a classe de teste.]
  Agora, você deve criar uma nova sessão chamada "▼ Tests" em sua aba de script,  abaixo da sessão "▼ Materials"

  Após feito isso, você deverá criar um novo arquivo chamado "Rtest" com o conteúdo do nosso script de test, ele tem o código fonte para que seja possível a criação da classe de testes (recomendo não editar).

 
# Autor: Resque
# E-mail: Rogessonb@gmail.com
# Data: 28/01/2017
# Engine: RPG Maker Ace VX

module RTeste
  class Teste
    extend Rmock

    def self.antes(&block)
      yield
    end

    def self.afirmar(valor)
      return true if valor == true
      mensagem_erro_padrao(true, valor)
    end

    def self.nao_afirmar(valor)
      return true if valor == false
      mensagem_erro_padrao(false, valor)
    end

    def self.afirmar_igualdade(isso, aquilo)
      return true if isso == aquilo

      mensagem_erro_padrao(isso, aquilo)
    end

    def self.afirmar_desigualdade(isso, aquilo)
      return true if isso != aquilo

      mensagem_erro_padrao(isso, aquilo)
    end

    def self.isso(nome_do_teste, &block)
      if yield == true
        print '.'
      else
        puts "Erro no teste: #{nome_do_teste}"
        puts yield
      end
    end

    private

    def self.mensagem_erro_padrao(aquilo, isso)
       "    - O valor esperado era: #{aquilo}, mas foi encontrado: #{isso}"
    end
  end
end
 



  Após feito isso, podemos criar o nosso teste para a classe Resque_Character_Name.
  Na aba "▼ Tests" você deve criar um novo código:
 
  class Resque_Character_Name_Test < RTeste::Teste
  end
 


  Sua aba deve ficar assim:

 

  Após feito isso, devemos definir o "sujeito" do nosso teste, ou seja, a classe que está sendo testada: Resque_Character_Name, vamos criar um bloco "antes" e iniciar a nossa classe testada, atribuindo ela para a variável @sujeito.

 
  class Resque_Character_Name_Test < RTeste::Teste
    antes do
      @sujeito = Resque_Character_Name.new
    end
  end
 


  Agora o nosso sujeito é uma instância da classe 'Resque_Character_Name', utilizaremos sempre o 'sujeito' para fazer os testes nos métodos da classe.

  Vamos criar o primeiro teste para a nossa classe.

  A classe testada, antes de tudo, deve descobrir o nome do personagem que será exibido.

  Vamos criar um teste para isso e ver ele falhar, pois a nossa classe ainda não tem a lógica para descobrir a informação do nome.

  Criamos esse teste dentro de um block "isso":

 
    class Resque_Character_Name_Test < RTeste::Teste
      antes do
        @sujeito = Resque_Character_Name.new
      end

      isso 'deve exibir o nome do personagem' do
        afirmar_igualdade @sujeito.character_name, 'Resque'
      end
    end
 


  A nossa classe testada, ainda não possúi o método "#character_name", vamos criar e deixar sem conteúdo.

 
    class Resque_Character_Name
      def character_name
      end
    end
 


  Salve tudo e feche a aba de scripts, agora rode o jogo e veja a informação do console:

    O console nos avisou que o teste quebrou, ele esperava o valor: Resque, mas não encontrou nenhum valor:

 

  Agora que sabemos disso, vamos fazer o nosso teste passar com o mínimo de lógica:

 
    class Resque_Character_Name
      def character_name
        'Resque'
      end
    end
 


  Após editar o script acima, salve e rode novamente o jogo.

 

  Podemos ver que o teste passou, foi exibido um ponto "." no console, e isso informa o sucesso do teste.

  Mas não é isso que queremos, devemos criar o algorítimo que busque o nome real do herói.
[/box2]

[box2 class=titlebg title=◊ Refatoração]
  Para isso, precisamos utilizar uma classe já existente no RGSS3: "Game_CharacterBase".

  A classe Game_CharacterBase possúi o nome do personagem, então vamos buscar ela passar ela como parâmetro na inicialização da classe Resque_Character_Name.

 
  class Resque_Character_Name
    def initialize(character)
      @character = character
    end

    def character_name
      'Resque'
    end
  end
 


  Vamos fazer o método #character_name buscar a informação do nome que é retornado da classe Game_CharacterBase:

 
  class Resque_Character_Name
    def initialize(character)
      @character = character
    end

    def character_name
      @character.character_name
    end
  end
 


  Feito isso, temos que atualizar o nosso teste, pois agora recebemos a classe Game_CharacterBase no initialize.

  Vamos inicializar a classe Game_CharacterBase, mudar o atributo character_name dela, e finalmente passar ela por parâmetro no nosso sujeito (Resque_Character_Name).


 
    class Resque_Character_Name_Test < RTeste::Teste
      antes do
        character = Game_CharacterBase.new
        character.character_name = 'Resque'

        @sujeito = Resque_Character_Name.new(character)
      end

      isso 'deve exibir o nome do personagem' do
        afirmar_igualdade @sujeito.character_name, 'Resque'
      end
    end
 

[/box2]

[box2 class=titlebg title=◊ Problemas =(]
  Ao executar o teste, recebemos o seguinte erro:
    Undefined method 'character_name=' for Game_CharacterBase.

  Isso quer dizer que o método 'character_name=' não existe para a classe Game_CharacterBase.

  Para resolver isso, precisamos criar um 'dublê' para a classe Game_CharacterBase.
[/box2]

[box2 class=titlebg title=◊ Rmock]
  Para auxiliar essa tarefa, criei uma classe chamada Rmock, que permitirá você criar uma classe com certos atributos para ajudar em nossos testes.

  Crie uma nossa sessão chamada ▼ Mocks.

 

  Dentro de ▼ Mocks, crie dois arquivos, Rmock e Game_CharacterBaseMock

  Dentro de RMock, adicione o código do script abaixo (não altere nada).

 
# Autor: Resque
# E-mail: Rogessonb@gmail.com
# Data: 28/01/2017
# Engine: RPG Maker Ace VX

module Rmock
  def self.define(klass_name, opt)
    name = opt[:as]
    Struct.new(klass_name)
    struct_class = Object.const_set("#{klass_name}", Struct.new(nil)).new

    self.create_instance_method(struct_class,name)

    yield struct_class

    self.create_class_method(struct_class, name)

    struct_class
  end

  private

  def self.create_instance_method(struct_class, name)
    struct_class.instance_eval do
      def self.method_missing(name, *args)
        self.class.instance_eval do
          define_method name do
            args.first
          end
        end
      end
    end
  end

  def self.create_class_method(struct_class, name)
    Object.class_eval do
      define_method name do
        struct_class
      end
    end
  end
end
 


  Dentro do arquivo 'Game_CharacterBaseMock', nós vamos criar o nosso dublê.

  No método define da classe Rmock, você deve passar o nome da classe que você quer dublar, e em as: você define um apelido para esse dublê, no caso eu usei 'game_character_base'.

  O terceiro parâmetro é um bloco que recebe um atributo seguindo de um valor.

  No nosso caso, vamos dublar o atributo character_name da classe Game_CharacterBase:

 
  Rmock.define 'Game_CharacterBase', as: 'game_character_base' do |mock|
    mock.character_name 'Resque'
  end
 


  Agora vamos alterar o nosso 'antes', passando o nosso dublê chamado 'game_character_base', e remover a inicialização da classe Game_CharacterBase.

  !Importante! o dublê ficará disponível dentro do seu teste em formato de variável usando o apelido dado para ele: game_character_base

 
    class Resque_Character_Name_Test < RTeste::Teste
      antes do
        @sujeito = Resque_Character_Name.new(game_character_base)
      end

      isso 'deve exibir o nome do personagem' do
        afirmar_igualdade @sujeito.character_name, 'Resque'
      end
    end
 


  Agora o nosso teste está passando novamente!!

  Podemos ter tranquilidade de modificar e melhorar a nossa classe, sabendo que se algo der erro, vamos ser informados na tela inicial do nosso jogo =)
[/box2]

[box2 class=titlebg title=◊ Conclusão]
Para não estender ainda mais o tutorial, estarei exibindo abaixo a classe totalmente funcional com as devidas coberturas de teste para que você analise e tente utilizar em seus próximos projetos.
[/box2]

[box2 class=titlebg title=◊ Recomendações]
Particularmente, eu busco utilizar os testes apenas em projetos onde quero reduzir os erros e manter a integridade.

  Por ser uma tarefa que consome tempo, não recomendo ser usada em Game Jam ou Duelos de Scripts, mas sim em jogos comerciais, onde erros podem trazer sérios problemas.
[/box2]

[box2 class=titlebg title=◊ Agradecimentos e Referências]
  Se você está lendo isso, saiba que estou muito feliz!!

  Testes são um tanto complexos e as vezes confusos no começo, mas nos ajudam muito no futuro do projeto.

  Algumas recomendações sobre testes automatizados de sistemas podem ser encontradas em:
    https://pt.wikipedia.org/wiki/Test_Driven_Development
    http://www.devmedia.com.br/test-driven-development-tdd-simples-e-pratico/18533
    http://www.devmedia.com.br/tdd-fundamentos-do-desenvolvimento-orientado-a-testes/28151
[/box2]

[box2 class=titlebg title=◊ Script de exibição do nome do personagem]
Game_CharacterBaseMock
Rmock.define 'Game_CharacterBase', as: 'game_character_base' do |mock|
  mock.character_name 'Resque'
  mock.screen_x  10
  mock.screen_y  6
end


Resque_Character_Name_Test
class Resque_Character_Name_Test < RTeste::Teste
  antes do
    @sujeito = Resque_Character_Name.new(game_character_base)
  end

  isso 'deve exibir o sprite do nome do personagem' do
    sprite = @sujeito.instance_variable_get(:@sprite)
    afirmar sprite.is_a? Sprite
  end

  isso 'deve exibir o bitmap do sprite do personagem' do
   sprite = @sujeito.instance_variable_get(:@sprite)
   afirmar sprite.bitmap.is_a? Bitmap
  end

  isso 'deve exibir o nome do personagem' do
    afirmar_igualdade @sujeito.send(:character_name), 'Resque'
  end

  isso 'deve ser verdadeiro apernas, quando a posição x ou y do sprite for diferente da posição do personagem' do
    afirmar @sujeito.instance_variable_get(:@need_refresh)

    @sujeito.update_sprite_position

    nao_afirmar @sujeito.instance_variable_get(:@need_refresh)
  end

  isso 'deve devolver o tamanho do nome do personagem' do
    afirmar @sujeito.send(:name_size) > 0
  end

  @sujeito = nil
end


Resque_Character_Name
class Resque_Character_Name
  def initialize(character)
    @character = character
    create_sprite

    @need_refresh = true
  end

  def update
    update_sprite_position
  end

  def update_sprite_position
    check_refresh

    return unless @need_refresh
    @sprite.x = @character.screen_x - 23
    @sprite.y = @character.screen_y

    @need_refresh = false
  end

  private

  def create_sprite
    @sprite        = Sprite.new
    @sprite.bitmap = Bitmap.new(name_size, 20)
    @sprite.bitmap.draw_text(0, 0, name_size, 20, "#{character_name}", 1)
  end

  def check_refresh
    @need_refresh = true if !sprite_same_y? || !sprite_same_x?
  end

  def sprite_same_x?
    @sprite.x == @character.screen_x
  end

  def sprite_same_y?
    @sprite.y == @character.screen_y
  end

  def name_size
    character_name.size * 9
  end

  def character_name
    @character.character_name
  end
end

class Scene_Map < Scene_Base
  def start
    super
    SceneManager.clear
    $game_player.straighten
    $game_map.refresh
    $game_message.visible = false
    create_spriteset
    create_all_windows
    @menu_calling = false
    @resque_character_name = Resque_Character_Name.new($game_player)
  end

  def update
    super
    $game_map.update(true)
    $game_player.update
    $game_timer.update
    @spriteset.update
    @resque_character_name.update
    update_scene if scene_change_ok?
  end
end


[/box2]

Dúvidas?