Mock static inserido via `@Mock` vaza?
Sim, eu sei, toda vida que você faz mock de método estático uma fadinha morre…
Mas eu tive uma situação bem particular no trabalho. Eu refatorei um pacote
para isolar uma dependência externa indesejada… e com isso em uma vez ao
rodar os testes no CI a pipeline carregou o classpath de teste em uma ordem
diferente (aleatoriamente, claro, não determinístico) carregou uma classe de
teste que fazia o mock de um método estático e então carregou uma outra classe
de teste que claramente esperava que o método estático retornasse dados
reais… e aí claramente que um 0 ou um null era um valor inválido no
contexto e quebrou com uma exceção maluca.
Recriando a falha
Bem, eu não posso depender da ordem de carregamento de classes no classpath se eu quiser reproduzir de modo garantido a falha, né? Então, vamos criar testes aninhados?
Primeiro, vamos ter nossa classe para mockar o valor:
public class Util {
static boolean alt = false;
public static String marm() {
return alt? "hmmmm": "mota";
}
}
Ok, agora vamos matar uma fadinha inocente e mockar o método estático?
Primeiro, usando a técnica clássica de mock com try-with-resources (tal qual
feito
neste post):
public class MockTest {
@Test
void t() {
try (MockedStatic<Util> mock = mockStatic(Util.class);) {
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
assertEquals("MARMOTAAAAA", Util.marm());
}
}
}
Show! Agora temos aqui uma situação em que o mock estático está funcionando. Próximo passo: ficar independente do classloader!
No JUnit, eu posso adicionar testes “aninhados” com @Nested. Então, posso
adicionar uma quantidade arbitrária de testes em um único arquivo! Então, vamos
adicionar um teste para o retorno mockado e outro para o retorno esperado?
Primeiro vamos fazer direito, e depois vamos fazer vazando:
public class MockTest {
@Nested
class C1 {
@Test
void t() {
try (MockedStatic<Util> mock = mockStatic(Util.class);) {
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
assertEquals("MARMOTAAAAA", Util.marm());
}
}
}
@Nested
class C2 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
}
Ok, os dois testes funcionaram sem problemas. Agora… vamos adicionar o
problema? Simplesmente remover o try-with-resources vai causar o vazamento!
public class MockTest {
@Nested
class C1 {
@Test
void t() {
MockedStatic<Util> mock = mockStatic(Util.class);
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
assertEquals("MARMOTAAAAA", Util.marm());
}
}
@Nested
class C2 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
}
Hmmm, não vazou. Mas, o que ocorreu de errado? Bem, executei pelo IntelliJ e ele me deu esse relatório:

Ele carregou de baixo pra cima…? E se eu adicionar um C0 antes do C1?
public class MockTest {
@Nested
class C0 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
@Nested
class C1 {
@Test
void t() {
MockedStatic<Util> mock = mockStatic(Util.class);
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
assertEquals("MARMOTAAAAA", Util.marm());
}
}
@Nested
class C2 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
}
Ok, agora a única chance de não detectar um vazamento é se o IntelliJ resolver
carregar com o JUnit o C1 apenas no final. Vamos ver se isso vai ocorrer?

Show! Falhou! Falhou como a gente queria?
org.opentest4j.AssertionFailedError:
Expected :mota
Actual :MARMOTAAAAA
FALHOU COMO QUERIA!!! Agora eu tenho uma detecção do vazamento!
Foram feitos mais testes que ficariam muito sacais recolocar aqui no post,
então como um resumo: o meu IntelliJ carregava os testes das classes @Nested
sempre de baixo para cima, ignorando o nome das classes em si.
Sobre a recriação da falha: eu recriei especificamente o vazamento do mock
estático, não exatamente que ele iria retornar nulo. Recriar o retorno nulo
seria simplesmente não colocar o mock.when(() -> ...).thenReturn(...). Mas
o jeito que foi escrito é suficiente para detectar vazamentos, o que é minha
intenção.
Injeção de @Mock
Uma estratégia clássica para evitar o trabalho de criar mocks na mão é usar a
anotação @Mock nos testes, em classes que estão anotadas com a extensão do
JUnit @ExtendWith(MockitoExtension.class). Basicamente ele facilita a criação
do objeto mockado. Por exemplo:
@ExtendWith(MockitoExtension.class)
public class SampleMockTest {
@Mock
Supplier<String> aaah;
@Test
void t() {
assertNull(aaah.get());
}
}
Viu como não foi inicializado nada no aaah? E eu simplesmente pude chamar
ele, indicando claramente que o objeto existe ali. Isso se dá porque a extensão
do Mockito injetou o mock corretamente.
Uma das coisas que o Mockito fornece com testes mais rígidos (o padrão) é bater na cabeça de quem mocka as coisas mas o mock não é usado:
@ExtendWith(MockitoExtension.class)
public class SampleMockTest {
@Mock
Supplier<String> aaah;
@Test
void t() {
when(aaah.get()).thenReturn("123");
// assertNull(aaah.get());
}
}
Isso dá o erro:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at com.jeffque.SampleMockTest.t(SampleMockTest.java:23)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
Injeção de @Mock vaza mock estático?
Bem, sobre o vazamento do mock estático, chegou um ponto no trabalho que foi
levantado o questionamento: se o MockedStatic<Util> for gerado pelo @Mock,
isso vai causar o vazamento não intencional?
O cenário do teste que gerou o vazamento do MockedStatic específico realmente
foi um MockedStatic gerado no corpo de uma função marcada com @BeforeAll
(ou seja, executado no começo do começo do teste específico). Esse cenário a
intenção aparente de quem escreveu o teste era que o método estático ficasse
mockado daquele ponto em diante para aquele teste INTEIRO, e que fosse
carregado de modo a impactar na carga de outras classes no classloader.
Portanto… talvez usar try-with-resources não fosse adequado para o cenário
de teste que o autor original estava prevendo.
Além disso, no teste específico que vazou o mock estático, existem cerca de
1500 linhas de código, o que é além da minha intenção de ter domínio para fazer
uma correção profunda. E nesse teste específico já existiam 11 mocks estáticos
injetados com @Mock. E claro que o ponto que era usado em outro teste era a
décima segunda classe mockada…
Meus colegas propuseram transformar tudo em try-with-resources, o que
causaria mudanças extensas nesse arquivo de teste e sem falar na alteração de
ordem de carregamento, o que talvez implicasse em comportamentos anômalos para
os mocks (afinal, mockar estático é matar fadinhas, e muitos sacrifícios de
fadas foram feitos nessas 1500 linhas). Já o meu instinto de “play-safe” era
fazer o mínimo de alterações necessárias para o funcionamento.
As alterações mínimas
Para funcionar com as alterações mínimas, preciso que o mock estático criado no
@BeforeAll fique armazenado em uma variável estática para que possa ser
liberado em um @AfterAll. O valor continuaria sendo criado no lugar correto,
mas ele não seria armazenado apenas na stack da função, mas sim no campo
estático.
Porém isso tem um pressuposto: NÃO HÁ VAZAMENTO de mock estático.
Montando o cenário de teste
Vamos resgatar o MockTest:
public class MockTest {
@Nested
class C0 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
@Nested
class C1 {
@Test
void t() {
try (MockedStatic<Util> mock = mockStatic(Util.class);) {
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
assertEquals("MARMOTAAAAA", Util.marm());
}
}
}
@Nested
class C2 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
}
Vamos transformar agora a carga do MockedStatic em uma injeção com @Mock?
Para isso, a preparação do mock vai ser colocada em um @BeforeEach só para
isolar o setup do teste propriamente dito:
@ExtendWith(MockitoExtension.class)
public class MockTest {
@Nested
class C0 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
@Nested
class C1 {
@Mock
private MockedStatic<Util> mock;
@BeforeEach
void b() {
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
}
@Test
void t() {
assertEquals("MARMOTAAAAA", Util.marm());
}
}
@Nested
class C2 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
}
Com resultados:

Isso significa que não houve vazamentos, confere? A gente pode adicionar uns testes a mais para refinar que não tem problemas, né?
@ExtendWith(MockitoExtension.class)
public class MockTest {
@Nested
class C0 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
@Nested
class C1 {
@Mock
private MockedStatic<Util> mock;
@BeforeEach
void b() {
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
}
@Test
void t() {
assertEquals("MARMOTAAAAA", Util.marm());
}
}
@Nested
class C2 {
@Mock
private MockedStatic<Util> mock;
@BeforeEach
void b() {
mock.when(() -> Util.marm()).thenReturn("teste teste");
}
@Test
void t() {
assertEquals("teste teste", Util.marm());
}
}
}
Ok, deu certo. Os dois mocks deram certo. E se… eu vazar os mocks no modo
clássico? Crio o MockedStatic na stack da função e deixo ela vazar?
@ExtendWith(MockitoExtension.class)
public class MockTest {
@Nested
class C0 {
@Test
void t() {
assertEquals("mota", Util.marm());
}
}
@Nested
class C1 {
@BeforeEach
void b() {
MockedStatic<Util> mock = mockStatic(Util.class);
mock.when(() -> Util.marm()).thenReturn("MARMOTAAAAA");
}
@Test
void t() {
assertEquals("MARMOTAAAAA", Util.marm());
}
}
@Nested
class C2 {
@BeforeEach
void b() {
MockedStatic<Util> mock = mockStatic(Util.class);
mock.when(() -> Util.marm()).thenReturn("teste teste");
}
@Test
void t() {
assertEquals("teste teste", Util.marm());
}
}
}

Ok, falhou de mais modos do que eu imaginava… vamos ver os erros?
Erro em C1:
org.mockito.exceptions.base.MockitoException:
For com.jeffque.Util, static mocking is already registered in the current thread
To create a new mock, the existing static mock registration must be deregistered
at com.jeffque.MockTest$C1.b(MockTest.java:34)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptLifecycleMethod(TimeoutExtension.java:128)
Erro em C0:
org.opentest4j.AssertionFailedError:
Expected :mota
Actual :teste teste
No caso do erro de C0, é apenas o vazamento comum. O mock estático preparado
em C2 não é o resultado esperado. Em C1, o setup falhou miseravelmente
ainda no nível de rodar antes do teste: To create a new mock, the existing
static mock registration must be deregistered.
Conclusão
Não é simples vazar o @Mock. Quando criado, o próprio Mockito dá um jeito de
desmontar ele. E no caso de @Mock MockedStatic<T>, o Mockito sabe que
desmontar esse objeto é dar um .close() para liberar as mudanças realizadas
no classloader.
Então, respondendo a questão: não, não é possível vazar um MockedStatic<T> ao
se usar o @Mock.
O código em questão
O código que tinha erro continha diversos @Mock com MockedStatic<T>. Mas
como vimos nos cenários de teste: esses mocks estáticos não vazam. Mas o teste
em questão tinha um ponto (claro, para a codebase em questão):
@BeforeAll
static void setup() {
MockedStatic<Util> mock = mockStatic(Util.class);
mock.when(() -> Util.marm()).thenReturn("teste teste");
}
E isso era de uma classe que seria utilizada em uma inicialização estática de
uma classe. E para deixar tudo mais intrigante: o método que gerava a derradeira
falha não era o que estava sendo mockado, mas sim um outro nada a ver que
retornava um inteiro, cujo valor de uso deveria ser sempre maior do que 1. E
o valor padrão para ausência de valores para inteiro primitivo? Zero. Todos os
bits zerados. De modo semelhante ao valor padrão para um tipo referência ser
sempre null.
E como as coisas são sempre bem emboladas: a classe que geraria o erro não é carregada, nem direta nem indiretamente, no teste que está preparando o mock estático.
Solução preguiçosa?
O primeiro pensamento é colocar como @Mock MockedStatic<T>. Porém… bem…
essa variável estava sendo preparada em um @BeforeAll, que é carregado junto
da classe, antes de se ter instância dela.
Como não se tinha ideia do caso de uso para o mock sendo realizado, e já se era
sabido que as alterações nessa classe sendo mockada tinha como efeito possível
mudar comportamento de classe sendo carregada, injetar ela como um @Mock de
instância poderia não ter o possível efeito adequado.
Por sinal, existe a possibilidade de se fazer static @Mock MockedStatic<T>, e
isso se comporta da maneira esperada: ele cria um mock diferente para cada
teste sendo executado:
@Nested
class C2 {
static @Mock MockedStatic<Util> mock;
@BeforeEach
void b() {
System.out.println("before " + System.identityHashCode(mock));
mock.when(() -> Util.marm()).thenReturn("teste teste");
}
@Test
void t1() {
System.out.println("test " + System.identityHashCode(mock));
assertEquals("teste teste", Util.marm());
}
@Test
void t2() {
System.out.println("test " + System.identityHashCode(mock));
assertEquals("teste teste", Util.marm());
}
}
Output:
before 238467882
test 238467882
before 1904273153
test 1904273153
E mais legal ainda: ao tentar usar com @BeforeAll:
@BeforeAll
static void b() {
mock.when(() -> Util.marm()).thenReturn("teste teste");
}
Tenho um clássico null pointer exception!
java.lang.NullPointerException: Cannot invoke "org.mockito.MockedStatic.when(org.mockito.MockedStatic$Verification)" because "com.jeffque.MockTest$C2.mock" is null
Ou seja: essa solução vai interferir o como que o carregamento dessa classe sendo mockada influencia em como outras classes sendo carregas pelo class loader.
Eu resolvo a questão de vazar o mock, mas não garanto que os efeitos colaterais desejados de mockar esse valor está realmente ocorrendo como deveria.
Solução com mínimo de mudanças
Para manter o mínimo de mudanças, o mock estático deveria sim acontecer no
mesmo momento. Porém, para lidar com o “fechamento” correto do teste, preciso
dar um close() no mock. Para fazer isso, preciso da referência ao
MockedStatic, para então no @AfterAll poder chamar o método correto. Logo:
// variável criada
private static MockedStatic<Util> utilMocked;
@BeforeAll
static void setup() {
// MockedStatic<Util> utilMocked = mockStatic(Util.class); <-- só resquícios do código original
utilMocked = mockStatic(Util.class);
utilMocked.when(() -> Util.marm()).thenReturn("teste teste");
}
// adicionando o fechamento
@AfterAll
static void teardown() {
utilMocked.close();
}
Pronto, agora o ciclo de vida da classe mockada continua sendo exatamente o
mesmo que se tinha para o teste. Porém, com uma diferença: ela não vaza mais!
Porque estou fechando ela no teardown.
Mas… vou ser sincero. A coisa… era mais feia… Porque não era só uma classe sendo mockada estática assim. Eram duas!! E as duas vazavam da mesma maneira! Logo, a solução seria isso, certo?
// variáveis criada
private static MockedStatic<Util> utilMocked;
private static MockedStatic<Util2> util2Mocked;
@BeforeAll
static void setup() {
// MockedStatic<Util> utilMocked = mockStatic(Util.class); <-- só resquícios do código original
utilMocked = mockStatic(Util.class);
utilMocked.when(() -> Util.marm()).thenReturn("teste teste");
// MockedStatic<Util2> util2Mocked = mockStatic(Util2.class); <-- só resquícios do código original
util2Mocked = mockStatic(Util2.class);
util2Mocked.when(() -> Util2.marm()).thenReturn("teste teste");
}
// adicionando o fechamento
@AfterAll
static void teardown() {
util2Mocked.close();
utilMocked.close();
}
Hmmm, na real? Não. Porque podem ter situações… absurdas… que podem ocorrer
em um .close(). Como por exemplo estourar exceção.
Eu espero que aconteçam? Não. Até porque a exceção documentada para isto
ocorrer é de tentar dar um .close() após um outro .close(). Mas aqui
estamos em um código que já deu problema por mal uso! Um problema bem chatinho
de detectar, mas já deu. Inserir um código com possível problema de detecção de
falha não estava nos meus planos.
Então, como podemos resolver isso? Que tal… criar um mecanismo para fechamento de todas as classes abertas?
Pois bem, vamos lá! Uma solução seria simplesmente assumir que só existem esses
2 MockedStatic<T> que precisam ser criados assim e lidar com isso com
diversos try-catch:
@AfterAll
static void teardown() {
try {
util2Mocked.close();
} catch (Exception e) {
}
utilMocked.close();
}
Mas isso engole potencialmente o erro no primeiro .close(), e não queremos
isso. Portanto, precisamos dar um jeito de manter o erro e disparar ele no
final!
@AfterAll
static void teardown() throws Exception {
Exception thrownErr = null;
try {
util2Mocked.close();
} catch (Exception e) {
thrownErr = e;
}
try {
utilMocked.close();
} catch (Exception e) {
thrownErr = e;
}
if (thrownErr != null) {
throw thrownErr;
}
}
Ainda podemos perder exceções? Sim, pois ambos os .close() podem lançar no
mesmo cenário! Então, qual o mecanismo que o Java fornece pra isso? Usar o
.addSuppressed(e):
@AfterAll
static void teardown() throws Exception {
Exception thrownErr = null;
try {
util2Mocked.close();
} catch (Exception e) {
thrownErr = e;
}
try {
utilMocked.close();
} catch (Exception e) {
if (thrownErr != null) {
thrownErr.addSuppressed(e);
} else {
thrownErr = e;
}
}
if (thrownErr != null) {
throw thrownErr;
}
}
Tá, isso ficou assimétrico… posso colocar umas coisinhas no primeiro catch
só pra ficar simétrico:
@AfterAll
static void teardown() throws Exception {
Exception thrownErr = null;
try {
util2Mocked.close();
} catch (Exception e) {
if (thrownErr != null) {
thrownErr.addSuppressed(e);
} else {
thrownErr = e;
}
}
try {
utilMocked.close();
} catch (Exception e) {
if (thrownErr != null) {
thrownErr.addSuppressed(e);
} else {
thrownErr = e;
}
}
if (thrownErr != null) {
throw thrownErr;
}
}
Mesmo sendo óbvio (tenho até o warning “Condition thrownErr != null is always
false”), isso fica mais simétrico e repetido. E isso me leva a outro ponto!
Podemos expandir isso! Posso colocar em uma lista e iterar em cima disso:
@AfterAll
static void teardown() throws Exception {
Exception thrownErr = null;
List<MockedStatic<?>> mocks = List.of(util2Mocked, utilMocked);
for (final var mock: mocks) {
try {
mock.close();
} catch (Exception e) {
if (thrownErr != null) {
thrownErr.addSuppressed(e);
} else {
thrownErr = e;
}
}
}
if (thrownErr != null) {
throw thrownErr;
}
}
Muito bom, agora isso seria um pedaço de código potencialmente reutilizável,
né? Se na base de códigos tivesse potencialmente mais vazamentos assim? Que
tal… fazer uma classe para isso? E também eu posso transformar isso em uma
função que retorna a exceção lançada e com isso fazer um .reduce, que tal?
Eu não sou fã de exceções, por isso me encanto tanto no jeito que Go lida com os erros como valores. Então, vamos primeiro lidar com essa questão das exceções como valores e depois isolar em um componente reutilizável?
Eu tenho aqui a transformação de um () -> void em Exception | null.
Isso significa: pegar um Runnable -> Exception | null? Hmm, não no Java, pois
as exceções checadas precisam necessariamente passar na assinatura da função e
o Runnable não possui isso na assinatura. Então posso criar minha própria
interface funcional para ignorar o que está rodando por baixo:
interface MayThrow {
void action() throws Exception;
}
E com isso eu consigo fazer um mapeamento de MayThrow -> Exception | null:
static Exception exceptionAsValue(MayThrow mt) {
try {
mt.action();
return null; // foi tudo bem
} catch (Exception e) {
return e;
}
}
E para fazer o reduce de exceções? Vamos supor que exceptionAsValue é um
método de MayThrow:
List<MayThrow> l = List.of(mt1, mt2, mt3, ...);
final var e = l.stream() // MayThrow
.map(MayThrow::exceptionAsValue) // Exception
.reduce(null, (acc, el) -> {
if (acc == null) {
return el;
}
if (el == null) {
return acc;
}
acc.addSuppressed(el);
return acc;
});
if (e != null) {
throw e;
}
Ok, parece razoável. Agora, para tornar isso reutilizável… vamos criar uma
função que recebe muito AutoCloseables? closeAll? Dentro de MayThrow, só
para deixar claro que talvez lance algo, mesmo fechando tudo…
E, bem. Que tal deixar resiliente contra nulos acidentais?
static void closeAll(AutoCloseable ...closeables) throws Exception {
final var e = Stream.of(closeables)
.filter(Objects::nonNull)
.map(ac -> (MayThrow) ac::close)
.map(MayThrow::exceptionAsValue)
.reduce(null, (acc, el) -> {
if (acc == null) {
return el;
}
if (el == null) {
return acc;
}
acc.addSuppressed(el);
return acc;
});
if (e != null) {
throw e;
}
}
Ok, e na hora de usar?
// variáveis criada
private static MockedStatic<Util> utilMocked;
private static MockedStatic<Util2> util2Mocked;
@BeforeAll
static void setup() {
// MockedStatic<Util> utilMocked = mockStatic(Util.class); <-- só resquícios do código original
utilMocked = mockStatic(Util.class);
utilMocked.when(() -> Util.marm()).thenReturn("teste teste");
// MockedStatic<Util2> util2Mocked = mockStatic(Util2.class); <-- só resquícios do código original
util2Mocked = mockStatic(Util2.class);
util2Mocked.when(() -> Util2.marm()).thenReturn("teste teste");
}
// adicionando o fechamento
@AfterAll
static void teardown() throws Exception {
MayThrow.closeAll(util2Mocked, utilMocked);
}
E foi essa a minha primeira proposta de correção! Antes do primeiro code review
em que propuseram trocar todos os campos @Mock por try-with-resources.
Afinal, código que não era meu, “play-safe”.
Anticlímax
Sabe o utilMocked.when(() -> Util.marm()).thenReturn("teste teste");? E o
util2Mocked.when(() -> Util2.marm()).thenReturn("teste teste");? Pois bem…
O teste funcionava do mesmo jeito, com ou sem o mock! Mesmo eventualmente se eu
prepara o mock estático e não utilizar ele, o mockito não reclama por ausência
de uso (diferente de um mock tradicional que, se eu mockar e não estiver em
modo leniente, reclama de “Unnecessary stubbings detected”). Então o mock não
estava sendo usado de modo algum no final das contas… a correção portanto foi
simplesmente apagar o método @BeforeAll.
O code review que recebi veio com uma reclamação sobre a complexidade do
MayThrow. Após inspecionar a codebase, esse vazamento de mock estático não
ocorria em nenhum outro lugar, todos os mocks estáticos estavam adequadamente
em um try-with-resources ou com o ciclo de vida controlado pelo @Mock.
Essa discussão sobre manter ou não o MayThrow acabou gerando a pergunta sobre
a necessidade de ter aquele método mockado ou não, mesmo o “play-safe” sendo
respeitado.
Considerações sobre o code review
Na revisão, os meus colegas sugeriram usar o try-with-resources junto de cada
mock estático. E, sinceramente? Eles estavam certos, e isso seria realmente o
mais adequado a se fazer pela saúde do código.
Porém… aquele código não era de meu domínio. E eu queria tocar o mínimo possível nele. Eu precisava apenas que ele não atrapalhasse os PRs das demais equipes, como ele atrapalhou o meu PR uma vez. Mas pelo “play-safe” é melhor deixar essa refatoração com quem deveria entender, com a equipe que realmente é a dona do código.
Mas, por que agora estou dizendo que o try-with-resources é a alternativa
certa? Justamente por conta do estrago feito em classloader!
Quando você usa o @Mock mockedStatic, aquilo toma efeito a cada teste sendo
executado. Se, sem querer, você acaba sobrescrevendo uma classe que
eventualmente vai ser chamada no carregamento de outra classe, já era, a classe
que está sendo carregada vai ficar maculada com resquícios do mock. E usar o
@Mock é um alcance bem grande para esse problema acontecer.
Quando você usa o try-with-resources, o efeito já vai estar localizado dentro
do método de teste específico. Isso quer dizer que vai impedir todo e qualquer
problema relacionado a carga de classes? Não, não quer dizer isso. Mas a classe
que vai ser maculada já fica maculada logo naquele instante, você tem menos
oportunidades de macular as coisas de modo não intencional.
O MayThrow.closeAll() que eu fiz inclusive foi baseado em como eu creio que
funcione o try-with-resources, com lembranças de bytecode decompilado para
ver as entranhas de como o Java lidava com as muitas exceções possíveis, porém
de um jeito mais funcional.
Ah! Quase esqueci de mencionar, mas isso eu carrego sempre no meu coração, e quero que você sempre se lembre disso: toda vida que você faz mock de método estático uma fadinha morre. Só faça o mock estático se realmente ele for mais necessário do que a vida de uma criatura mágica da natureza.