O que é DSL e por que você precisa dela?
\n\nImagine que você não está escrevendo código, mas sim uma instrução em linguagem clara que se transforma em um programa funcional. É exatamente assim que funcionam as linguagens específicas de domínio (DSL — Domain Specific Language). Diferente das linguagens de uso geral (Java, Python, C#), uma DSL é criada para resolver uma única tarefa específica — seja configuração de servidor, descrição de regras de negócio ou construção de consultas SQL complexas.
\n\nKotlin é o candidato ideal para criar DSLs. Graças às lambdas com receptor, extensões e type-safe builders, você pode escrever código que parece sintaxe nativa. Neste artigo, vamos abordar não apenas a teoria, mas criar uma DSL real para um framework de testes — e você verá como é simples e empolgante.
\n\nPor que Kotlin é o rei das DSLs?
\n\nKotlin conquistou o amor dos desenvolvedores não apenas por sua compatibilidade com Java. Suas capacidades sintáticas permitem criar DSLs internas — linguagens que vivem dentro da linguagem hospedeira e usam sua sintaxe. Aqui estão os três pilares que sustentam qualquer DSL em Kotlin:
\n\n- \n
- Lambdas com receptor — permitem acessar métodos de um objeto sem especificar explicitamente seu nome. Esta é a base do padrão builder. \n
- Extensões — adicionam novas funções a qualquer classe, mesmo que você não possa modificá-la. \n
- Funções infixas — permitem escrever código no estilo de linguagem natural:
should be equalTo 5. \n
Essas capacidades transformam a criação de DSLs em um conjunto de Lego: você pega blocos prontos — lambdas, funções, propriedades — e monta uma linguagem clara e segura.
\n\nCriando uma DSL simples para testes: passo a passo
\n\nVamos escrever uma DSL para escrever testes. O objetivo é que os testes pareçam uma descrição legível, e não um conjunto de chamadas assert:
\n\ntest("A calculadora deve somar números") {\n val result = Calculator.add(2, 3)\n result should beEqualTo 5\n result shouldNot beNull\n}\n\nConcorde que isso é quase inglês. Agora vamos entender como funciona.
\n\nPasso 1. Criando o contexto do teste
\n\nPrecisamos de uma função test que recebe um nome e um bloco de código. O bloco é executado em um contexto onde should e shouldNot estão disponíveis:
class TestContext {\n fun Any.should(matcher: Matcher) {\n if (!matcher.matches(this)) {\n throw AssertionError("Esperava ${matcher.expected}, mas obteve $this")\n }\n }\n\n fun Any.shouldNot(matcher: Matcher) {\n if (matcher.matches(this)) {\n throw AssertionError("Não esperava ${matcher.expected}, mas obteve $this")\n }\n }\n}\n\nfun test(name: String, block: TestContext.() -> Unit) {\n println("Executando teste: $name")\n try {\n TestContext().block()\n println("✓ Aprovado")\n } catch (e: AssertionError) {\n println("✗ Reprovado: ${e.message}")\n }\n}\n\nPonto chave: TestContext.() -> Unit é uma lambda com receptor. Dentro do bloco, this se refere ao objeto TestContext, então os métodos should e shouldNot ficam implicitamente disponíveis.
Passo 2. Implementando os matchers
\n\nAgora vamos criar matchers simples. beEqualTo e beNull são funções que retornam objetos Matcher:
interface Matcher {\n val expected: String\n fun matches(value: Any?): Boolean\n}\n\nfun beEqualTo(expected: Any) = object : Matcher {\n override val expected = "igual a $expected"\n override fun matches(value: Any?) = value == expected\n}\n\nval beNull = object : Matcher {\n override val expected = "nulo"\n override fun matches(value: Any?) = value == null\n}\n\n