Qualidade e Teste de Código - RGSS Avançado
? 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
? 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.
? 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?
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).
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.
? 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?
? 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.
? 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:
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.
? 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:
Código:
antes do
@heroi = Hero.new
end
.afirmar
Recebe um parâmetro (verdadeiro ou falso). É utilizado para verificar se um valor é verdadeiro (true).
Ex:
Código:
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.
Código:
nao_afirmar @heroi.andando?
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:
Código:
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:
Código:
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:
Código:
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.
? 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).
Código:
# 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:
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.
Código:
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":
Código:
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.
Código:
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:
Código:
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.
? 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.
Código:
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:
Código:
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).
Código:
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
? 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.
? 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).
Código:
# 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:
Código:
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
Código:
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 =)
? 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.
? 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.
? 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
? Script de exibição do nome do personagem
Game_CharacterBaseMock
Código:
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
Código:
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
Código:
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