Desafio Go

Média com erro e subtestes paralelos

Enunciado

Implemente Media(nums []int) (float64, error) que retorna a média dos números e um erro se a slice estiver vazia. Escreva testes table-driven com t.Run cobrindo: slice com valores, slice vazia (erro) e slice com um elemento. Use t.Helper em um helper de comparação de floats com tolerância e chame t.Parallel() em cada subteste.

Requisitos

  • Media retorna erro quando a slice está vazia.
  • O helper de comparação usa t.Helper() e tolerância (ex.: 1e-9).
  • Cada caso roda em t.Run e chama t.Parallel().
  • A tabela inclui um campo querErro bool para distinguir casos de erro.

Código inicial

package estat

import (
	"errors"
	"testing"
)

func Media(nums []int) (float64, error) {
	// implemente
	return 0, nil
}

func verificaFloat(t *testing.T, esperado, obtido float64) {
	// implemente com t.Helper()
}

func TestMedia(t *testing.T) {
	// implemente
}

Saída esperada

=== RUN   TestMedia
=== RUN   TestMedia/valores
=== PAUSE TestMedia/valores
=== RUN   TestMedia/vazia
=== PAUSE TestMedia/vazia
=== RUN   TestMedia/um_elemento
=== PAUSE TestMedia/um_elemento
=== CONT  TestMedia/valores
=== CONT  TestMedia/vazia
=== CONT  TestMedia/um_elemento
--- PASS: TestMedia (0.00s)
    --- PASS: TestMedia/valores (0.00s)
    --- PASS: TestMedia/vazia (0.00s)
    --- PASS: TestMedia/um_elemento (0.00s)
PASS
ok  	estat	0.002s
Ver dica

Verifique len(nums) == 0 antes de somar. Para comparar floats, use math.Abs(esperado-obtido) > 1e-9.

Mostrar solução
package estat

import (
	"errors"
	"math"
	"testing"
)

// Media retorna a média aritmética dos números; erro se a slice estiver vazia.
func Media(nums []int) (float64, error) {
	if len(nums) == 0 {
		return 0, errors.New("slice vazia")
	}
	soma := 0
	for _, n := range nums {
		soma += n
	}
	return float64(soma) / float64(len(nums)), nil
}

// verificaFloat compara floats com tolerância; t.Helper aponta para o chamador.
func verificaFloat(t *testing.T, esperado, obtido float64) {
	t.Helper()
	if math.Abs(esperado-obtido) > 1e-9 {
		t.Errorf("esperado %v, obtido %v", esperado, obtido)
	}
}

func TestMedia(t *testing.T) {
	casos := []struct {
		nome     string
		nums     []int
		esperado float64
		querErro bool
	}{
		{"valores", []int{2, 4, 6}, 4, false},
		{"vazia", nil, 0, true},
		{"um elemento", []int{7}, 7, false},
	}

	for _, c := range casos {
		t.Run(c.nome, func(t *testing.T) {
			t.Parallel() // subtestes independentes podem rodar em paralelo
			obtido, err := Media(c.nums)
			if c.querErro {
				if err == nil {
					t.Fatalf("esperava erro, mas não veio")
				}
				return
			}
			if err != nil {
				t.Fatalf("erro inesperado: %v", err)
			}
			verificaFloat(t, c.esperado, obtido)
		})
	}
}

Passo a passo

  1. Media valida len(nums) == 0 e retorna errors.New("slice vazia").
  2. Somamos com um loop simples e dividimos convertendo para float64.
  3. O helper verificaFloat chama t.Helper() para que o erro aponte o teste chamador.
  4. Comparamos floats com tolerância 1e-9, evitando problemas de precisão binária.
  5. A tabela tem um campo querErro que diferencia casos de sucesso e de erro.
  6. Dentro do t.Run, chamamos t.Parallel() para permitir execução concorrente dos subtestes.
  7. Se o caso espera erro, usamos t.Fatalf quando ele não vem; senão, retornamos cedo.
  8. No caminho de sucesso, chamamos o helper para verificar o valor.

Por que funciona

t.Parallel() marca o subteste para rodar em paralelo com outros, o que acelera suítes grandes. Como cada subteste só lê dados da tabela, não há compartilhamento de estado mutável — condição essencial para paralelismo seguro.

O campo querErro mantém o padrão table-driven mesmo quando os resultados têm formas diferentes (número + erro). O helper com t.Helper() garante que, ao falhar, o relatório mostre a linha do t.Run, não a do helper.

Erros comuns

  • Esquecer t.Parallel() dentro do t.Run: o subteste roda sequencialmente; nada quebra, mas você perde o ganho.
  • Comparar floats com == diretamente: 0.1+0.2 != 0.3; use tolerância.
  • Mutar a slice c.nums dentro do subteste paralelo: gera corrida de dados. Trate os dados como somente leitura.
  • Usar t.Fatal dentro de uma goroutine que não é do teste — só chame Fatal na goroutine do subteste.

Outra forma de resolver

Sem paralelismo, rodando tudo sequencialmente:

for _, c := range casos {
	t.Run(c.nome, func(t *testing.T) {
		// sem t.Parallel()
	})
}

É preferível quando os testes compartilham recursos (arquivos, banco) ou quando o tempo total já é baixo.

Saída esperada

=== RUN   TestMedia
=== RUN   TestMedia/valores
=== PAUSE TestMedia/valores
=== RUN   TestMedia/vazia
=== PAUSE TestMedia/vazia
=== RUN   TestMedia/um_elemento
=== PAUSE TestMedia/um_elemento
=== CONT  TestMedia/valores
=== CONT  TestMedia/vazia
=== CONT  TestMedia/um_elemento
--- PASS: TestMedia (0.00s)
    --- PASS: TestMedia/valores (0.00s)
    --- PASS: TestMedia/vazia (0.00s)
    --- PASS: TestMedia/um_elemento (0.00s)
PASS
ok  	estat	0.002s