TV Digital

Desenvolvimento dirigido por testes para TV digital utilizando Lua

O Desenvolvimento Dirigido por Testes ou em inglês Test Driven Development (TDD) é uma técnica de desenvolvimento de software muito utilizada em...

· 8 min leitura >

O Desenvolvimento Dirigido por Testes ou em inglês Test Driven Development (TDD) é uma técnica de desenvolvimento de software muito utilizada em projetos que adotam metodologias ágeis, como SCRUM e XP, para gerenciamento.

Em TV digital o desenvolvimento também pode, e deve, ser ágil. Por isso decidi escrever este tutorial abordando uma solução para a realização de testes unitários em projetos de TV digital.

Neste tutorial apresentarei uma ferramente para a realização de testes unitários em código Lua e desenvolverei uma solução para possibilitar o uso da mesma ferramenta em projetos de TV digital.

Nosso ambiente de trabalho será composto pelo código a ser testado, os testes e uma ferramenta para execução dos testes e apresentação dos resultados. Vale mencionar que focarei em código Lua porque não encontrei, e acredito que ainda não foi lançada, uma ferramenta para a execução de testes em código NCL. Seria bem interessante utilizarmos  Desenvolvimento Dirigidos por Comportamento ou em inglês Behavior Driven Development (BDD) para projetos em NCL.

Ferramenta para execução dos testes

Existem várias opções de ferramentas para testes unitários em Lua. Nós utilizaremos o Telescope pelo fato dele possuir funcionalidades interessantes e uma API bem documentada.

Para instalar o Telescope, com o Luarocks previamente instalado, basta digitar:

sudo luarocks build telescope --from=http://luarocks.luaforge.net/rocks-cvs

Também é possível obter o código fonte a partir do repositório git:

git clone git://github.com/norman/telescope.git

Após a instalação podemos chamar o Telescope com o comando tsc. O help é bem explicativo e pode ser chamado com o comando abaixo:

tsc --help

Criando os primeiros testes

Neste primeiro exemplo criaremos um único arquivo com as funções de soma e subtração e os seus testes. O código pode ser observado abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- Funcoes que desejamos testar
function soma(a,b)
	return a+b
end
 
function sub(a,b)
	return a-b
end 
 
-- Testes para a funcao soma
context("Funcao soma(a,b)", function()
	test("Teste 1 + 1 = 2", function()
		assert_equal(soma(1,1), 2)
	end)
	test("Teste 1 + 's' = 0", function()
		assert_equal(soma(1,'s'),0)
	end)
end)
-- Testes para a funcao sub
context("Funcao sub(a,b)", function()
	test("Teste 1 - 1 = 0", function()
		assert_equal(sub(1,1),0)
	end)
	test("Teste 2 - 1 = 1", function()
		assert_equal(sub(2,1),1)
	end)
end)

Até a linha 9 não temos novidades. Na linha 11 iniciamos a declaração do contexto onde iremos organizar os testes para a função soma.

Nesse momento vale abrir uma parênteses e falar um pouco sobre os contextos. Como disse acima, o Telescope apresenta funcionalidades interessantes e uma delas é a possibilidade de organizarmos os testes em contextos, que ainda podem ser aninhados. No exemplo não utilizamos contextos aninhados porque os testes são bem simples, porém no código abaixo podemos ver o uso de contextos em outro exemplo com uma complexidade um pouco maior:

-- tests
context("Testing servers/api.lua", function()
--------
	context("Teste de conexao", function()
		local s,d,h,c = client.get(string.format("http://%s:%d", host, port), nil, auth)
	-----
		if not s then
			test("Sem conexao", function()
				assert_false(s)
				assert_equal(c, "connection refused")
				assert_blank(d)
				assert_nil(h)
			end)
		end
	-----
		if s then
			test("Com conexao", function()
 
				assert_true(s)
				assert_type(c, "number") --deve retornar um codigo
				assert_type(h, "table") -- deve retornar uma tabela com o cabecalho
			end)
		end
	-----
	end)
----------
	context("Teste de recursos", function()
	---------
		test("Recurso INVALIDO", function()
			local s,d,h,c = client.get(string.format("http://%s:%d/%s", host, port,"xyz"), nil, auth)
			assert_true(s)
			assert_type(c, "number") --deve retornar um codigo
			assert_equal(c, 404)
		end)
	--------
		test("Recurso PING disponivel", function()
			local s,d,h,c = client.get(string.format("http://%s:%d/%s", host, port,"ping"), nil, auth)
			assert_true(s)
			assert_type(c, "number") --deve retornar um codigo
			assert_not_equal(c, 404)
		end)
	--------
		test("Recurso SEARCH disponivel", function()
			local s,d,h,c = client.get(string.format("http://%s:%d/%s", host, port,"search"), nil, auth)
			assert_true(s)
			assert_type(c, "number") --deve retornar um codigo
			assert_not_equal(c, 404)
		end)
	--------
 
	end)
----------
end)

Esse código é parte de um arquivo de testes de um um servidor Lua que implementa uma API e oferece alguns recursos. Como pode-se notar, os contextos mostram-se bem úteis para a organização dos testes de um projeto quando começamos a trabalhar em algo mais elaborado.

Voltando ao nosso primeiro exemplo, a sintaxe básica, a partir da linha 11, para construirmos nossos testes é:

context("Nome do contexto", function()
	test("Nome do teste", function()
		-- Assertions
	end)
	--Mais testes
end)

O Telescope oferece por padrão as assertions mais básicas (veja a API). Porém, se desejar, você também pode construir as suas.

Para executarmos nosso teste fazemos:

tsc nome-do-arquivo.lua

A saída será algo como:

1
2
3
4
5
6
7
8
9
4 tests 3 passed 3 assertions 0 failed 1 error 0 unassertive 0 pending
 
Teste 1 + 's' = 0:
teste1.lua:4: attempt to perform arithmetic on local 'b' (a string value)
stack traceback:
   /usr/local/share/lua/5.1//telescope.lua:374: in function 'invoke_test'
  /usr/local/share/lua/5.1//telescope.lua:399: in function 'run'
  ...usr/local/lib/luarocks/rocks/telescope/scm-1/bin/tsc:266: in main chunk
  [C]: ?

Na lina 1 temos o resumo dos testes, na linha 3 o teste que falhou e na linha 4 o motivo da falha. A partir da linha 5 temos a saída do interpretador Lua.

Podemos melhorar nossa saída com o parâmetro -f:

tsc -f nome-do-arquivo.lua

Obtendo com isso uma saída mais detalhada, onde o Telescope apresenta um resumo mais elaborado dos testes realizados (linhas 1 a 9):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
------------------------------------------------------------------------
Funcao soma(a,b):
Teste 1 + 1 = 2                                                      [P]
Teste 1 + 's' = 0                                                    [E]
------------------------------------------------------------------------
Funcao sub(a,b):
Teste 1 - 1 = 0                                                      [P]
Teste 2 - 1 = 1                                                      [P]
------------------------------------------------------------------------
4 tests 3 passed 3 assertions 0 failed 1 error 0 unassertive 0 pending
 
Teste 1 + 's' = 0:
teste1.lua:4: attempt to perform arithmetic on local 'b' (a string value)
stack traceback:
  /usr/local/share/lua/5.1//telescope.lua:374: in function 'invoke_test'
  /usr/local/share/lua/5.1//telescope.lua:399: in function 'run'
  ...usr/local/lib/luarocks/rocks/telescope/scm-1/bin/tsc:266: in main chunk
	[C]: ?

Rebuscando um pouco mais

No exemplo anterior criamos as funções e os testes no mesmo arquivo. Porém, na prática, isso não funciona muito bem e nem deve ser feito.

Vamos separar nossos arquivos encapsulando as funções em um módulo. Assim podemos chamar o módulo diretamente do nosso arquivo de testes.

Nosso módulo de operações matemáticas ficará assim:

-- Modulo com operacoes matematicas
module("matematica")
-- operacao soma
function soma(a,b)
	return a+b
end
-- operacao subtracao
function sub(a,b)
	return a-b
end

E agora os testes em outro arquivo:

-- Chama o modulo matematica
require"matematica"
-- Testes
context("Funcao soma(a,b)", function()
	test("Teste 1 + 1 = 2", function()
		assert_equal(matematica.soma(1,1), 2)
	end)
	test("Teste 1 + 's' = 0", function()
		assert_equal(matematica.soma(1,'s'),0)
	end)
end)
context("Funcao sub(a,b)", function()
	test("Teste 1 - 1 = 0", function()
		assert_equal(matematica.sub(1,1),0)
	end)
	test("Teste 2 - 1 = 1", function()
		assert_equal(matematica.sub(2,1),1)
	end)
end)

Utilizando testes nos projetos de TV digital

Agora que já vimos como construir nossos testes iremos aplicar nosso conhecimento num exemplo que envolve desenvolvimento para TV digital.

Tomemos uma aplicação muito simples: “Quando o telespectador pressiona o botão vermelho uma mensagem é desenhada na tela e uma âncora é iniciada”.

Neste exemplo teremos um nó Lua com um tratador de eventos que só está interessado em eventos de pressionamento de tecla, mais especificamente só da tecla vermelha. Por não ser o objetivo deste tutorial, não entrarei em detalhes do código NCL.

Nosso tratador de eventos pode ser implementado assim:

function handler (evt)
  if (evt.class == 'key') and (evt.type == 'press') and (evt.key == 'RED') then
      canvas:attrColor('white')
      canvas:attrFont('vera',30)
      canvas:drawText (200, 200, "Botao vermelho pressionado." )
 
      event.post {
        class  = 'ncl',
        type   = 'presentation',
        area   = 'fim',
        action = 'start',
      }
 
  end
end
event.register(handler)

Iremos elaborar alguns testes para garantir a consistência na implementação do tratador de eventos. Para exemplificar, vamos definir algumas condições de contorno e posteriormente construiremos os testes para elas. Vale a ressalva de que esse procedimento não está 100% de acordo com os “mandamentos” (“…inicialmente o desenvolvedor escreve um teste e depois escreve o código que possa ser validado pelo teste…”) do TDD, porém como o objetivo aqui não é ser doutrinador-xiita, vamos focar na ferramenta e na solução; os mais rigorosos podem começar pelos testes sem problemas :-).

Nossas condições de contorno são:

  1. o tratador só deve tratar eventos de pressionamento de tecla;
  2. o tratador só está interessado na tecla vermelha;
  3. quando a tecla vermelha for pressionada a frase “Botao vermelho pressionado.” deve ser escrita na posição 200 x 200 e
  4. um evento (com  class = ‘ncl’, type = ‘presentation’, area = ‘fim’ e action = ‘start’) deve ser postado;
  5. o tratador de eventos deve ser registrado.

Com nossas condições de contorno definidas podemos continuar. Porém o código do tratador de eventos não irá funcionar fora do middleware. Ao executá-lo recebemos o erro:

lua: exemplo.lua:16: attempt to index global 'event' (a nil value)
stack traceback:
	tratador.lua:16: in main chunk
	[C]: ?

Isso acontece porque algumas bibliotecas utilizadas não são padrões do Lua e só estão presentes na implementação do middleware. Para contornarmos essa situação, iremos realizar algumas configurações adicionais no arquivo de testes e assim garantir que eles funcionem fora do ambiente do middleware.

Logo no início do nosso arquivo de testes criaremos as configurações para simularmos os módulos utilizados pelo tratador de eventos.

O arquivo completo pode ser visto abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
-------
-- Tabelas representando os valores do sistema
-------
system_modified = false
-- Tela
canvas_values = {
	color = '',
	font = '',
	font_size = 0,
	text_x = 0,
	text_y = 0,
	text_message = '',
}
-- Eventos
events ={
	posted = {},
	event_registered = ''
}
-------
-- Configuracao modulo event
-------
event = {}
_G["event"] = event
-- Se recebe um tratador (funcao) registra e retorna true
function event.register(handler)
	if type(handler) == 'function' then
		events.event_registered = handler
		return true
	else return false end
end
-- Posta os eventos
function event.post(tab)
	if type(tab) == 'table' then
		events.posted.class = tab.class
		events.posted.type = tab.type
		events.posted.area = tab.area
		events.posted.action = tab.action
		system_modified = true
		return true
	else return false end
end
-------
-- Configuracao modulo canvas
-------
canvas = {}
_G["canvas"] = canvas
function canvas:attrColor(color)
	canvas_values.color = color
	system_modified = true
end
function canvas:attrFont(font,size)
	canvas_values.font = font
	canvas_values.font_size = size
	system_modified = true
end
function canvas:drawText(x,y,message)
	canvas_values.text_x = x
	canvas_values.text_y = y
	canvas_values.text_message = message
	system_modified = true
end
 
--------
-- Carrega o tratador de eventos
dofile"main.lua"
--------
 
--------
-- Inicio dos Testes
--------
context("Exemplo de testes para tratador de eventos", function()
 
	test("Teste 1: Tratador NAO trata outro tipo de evento", function()
		--configuracao do evento
		local evt = {
			class = "ncl",
			type = "presentation",
			action = "start"
		}
		system_modified = false
		handler(evt)
		assert_false(system_modified)
	end)
----------------
	test("Teste 2: So trata eventos quando a tecla vermelha (RED) for pressionada", function()
		--configuracao do evento
		local evt = {
			class = "key",
			type = "press",
			key = "RED"
		}
		system_modified = false
		handler(evt)
		assert_true(system_modified)
	end)
----------------
	test("Teste 3: Frase: 'Botao vermelho pressionado.' eh exibida", function()
		--configuracao do evento
		local evt = {
			class = "key",
			type = "press",
			key = "RED"
		}
		handler(evt)
		assert_equal(canvas_values.color,"white")
		assert_equal(canvas_values.font,"vera")
		assert_equal(canvas_values.font_size,30)
		assert_equal(canvas_values.text_x,200)
		assert_equal(canvas_values.text_y,200)
		assert_equal(canvas_values.text_message,"Botao vermelho pressionado.")
	end)
------------------
	test("Teste 4: Tratador posta evento", function()
		--configuracao do evento
		local evt = {
			class = "key",
			type = "press",
			key = "RED"
		}
		handler(evt)
		assert_equal(events.posted.class,"ncl")
		assert_equal(events.posted.type,"presentation")
		assert_equal(events.posted.area,"fim")
		assert_equal(events.posted.action,"start")
	end)
------------------
	test("Teste 5: Tratador de eventos esta registrado", function()
		assert_equal(events.event_registered,handler)
	end)
end)

Até a linha 61 temos as configurações para os testes. O que fiz foi criar algumas tabelas que representam o estado do sistema e funções que modificam esse estado. Neste caso as funções tem o mesmo nome das que estão nas bibliotecas para TV digital. Na linha 65 o arquivo com o tratador de eventos é carregado e da linha 70 até o final os testes são realizados.

Desta forma, quando o tratador de eventos é chamado (linha 81 por exemplo) o interpretador Lua chamará as funções que foram criadas no início do arquivo. Essas funções modificam o estado das tabelas que representam o sistema, assim podemos verificar se o tratador conseguiu realizar o que desejávamos apenas checando o valor dessas tabelas.

Nenhuma modificação foi realizada no código do tratador de eventos de forma que ele funcionará normalmente quando for executado pelo middleware.

Evite repetição de código

Para testes em um único tratador de eventos que está em um único arquivo esse exemplo atende nossas necessidades. Contudo se tivermos vários arquivos separados com vários tratadores de eventos começamos perceber que copiar um cabeçalho de configuração para cada aquivo pode ser algo, além de chato, que certamente causará problemas para manutenção.

Para solucionar esse problema poderíamos (é, não vamos estender esse tutorial ainda mais) criar um único módulo com as configurações necessárias e importá-lo em cada arquivo de testes. Fica a dica.

Conclusão

Nesse tutorial apresentei uma ferramenta para a realização de testes em código Lua e desenvolvemos uma metodologia para utilização destes testes nos projetos de TV digital. Espero que essa informação possa ser útil nos seus projetos.

Utilize os comentários para enriquecermos ainda mais as informações que foram apresentadas. Portanto se encontrou algum erro, tem alguma dúvida ou mesmo qualquer comentário, não deixe de se expressar.

Deixe seu comentário: