Como testar o código dependente de variáveis ​​de ambiente usando o JUnit?

140

Eu tenho um pedaço de código Java que usa uma variável de ambiente e o comportamento do código depende do valor dessa variável. Eu gostaria de testar esse código com diferentes valores da variável de ambiente. Como posso fazer isso no JUnit?

Eu já vi algumas maneiras de definir variáveis ​​de ambiente em Java em geral, mas estou mais interessado no aspecto do teste de unidade, especialmente considerando que os testes não devem interferir um com o outro.

vitaut
fonte
Como se trata de teste, a regra de teste da unidade Regras do sistema pode ser a melhor resposta no momento.
Atifm 12/06
3
Apenas para aqueles interessados na mesma pergunta ao usar JUnit 5: stackoverflow.com/questions/46846503/...
Felipe Martins Melo

Respostas:

199

A biblioteca System Rules fornece uma regra JUnit para definir variáveis ​​de ambiente.

import org.junit.contrib.java.lang.system.EnvironmentVariables;

public class EnvironmentVariablesTest {
  @Rule
  public final EnvironmentVariables environmentVariables
    = new EnvironmentVariables();

  @Test
  public void setEnvironmentVariable() {
    environmentVariables.set("name", "value");
    assertEquals("value", System.getenv("name"));
  }
}

Isenção de responsabilidade: sou o autor das Regras do sistema.

Stefan Birkner
fonte
1
Estou usando isso como @ClassRule. Preciso redefini-lo ou limpá-lo após o uso; se sim, como?
Mritunjay 26/07/16
Você não precisa. As variáveis ​​de ambiente originais são redefinidas automaticamente pela regra após a execução de todos os testes na classe.
27616 Stefan Birkner
Essa abordagem funciona apenas para o JUnit 4 ou versão superior. Não recomendado para JUnit 3 ou versão inferior ou se você misturar JUnit 4 e JUnit 3.
RLD
2
import org.junit.contrib.java.lang.system.EnvironmentVariables;Você precisará adicionar a dependência com.github.stefanbirkner:system-rulesno seu projeto. Está disponível no MavenCentral.
31919 Jean Jean
2
Aqui estão as instruções para adicionar a dependência: stefanbirkner.github.io/system-rules/download.html
Guilherme Garnier
77

A solução usual é criar uma classe que gerencia o acesso a essa variável ambiental, que você pode zombar na sua classe de teste.

public class Environment {
    public String getVariable() {
        return System.getenv(); // or whatever
    }
}

public class ServiceTest {
    private static class MockEnvironment {
        public String getVariable() {
           return "foobar";
        }
    }

    @Test public void testService() {
        service.doSomething(new MockEnvironment());
    }
}

A classe em teste obtém a variável de ambiente usando a classe Environment, não diretamente de System.getenv ().

Matthew Farwell
fonte
1
Eu sei que esta pergunta é antiga, mas eu queria dizer que esta é a resposta correta. A resposta aceita incentiva o design deficiente, com uma dependência oculta do System, enquanto que essa resposta incentiva um design adequado, tratando o System como apenas outra dependência que deve ser injetada.
Andrew
30

Em uma situação semelhante à essa, em que tive que escrever um caso de teste que depende da variável de ambiente , tentei o seguinte:

  1. Eu fui para as Regras do sistema, conforme sugerido por Stefan Birkner . Seu uso foi simples. Mas, mais cedo ou mais tarde, achei o comportamento irregular. Em uma execução, ele funciona, na execução seguinte falha. Eu investiguei e descobri que as Regras do Sistema funcionam bem com o JUnit 4 ou versão superior. Mas, nos meus casos, eu estava usando alguns Jars que dependiam do JUnit 3 . Então eu pulei as Regras do Sistema . Mais sobre isso, você pode encontrar aqui a anotação @Rule não funciona ao usar o TestSuite no JUnit .
  2. Em seguida, tentei criar a variável de ambiente por meio da classe Process Builder fornecida por Java . Aqui, através do Java Code, podemos criar uma variável de ambiente, mas você precisa saber o nome do processo ou do programa que eu não conheci . Também cria variável de ambiente para o processo filho, não para o processo principal.

Perdi um dia usando as duas abordagens acima, mas sem sucesso. Então Maven veio em meu socorro. Podemos definir variáveis ​​de ambiente ou propriedades do sistema através do arquivo POM do Maven, que acho a melhor maneira de fazer o teste de unidade para o projeto baseado no Maven . Abaixo está a entrada que fiz no arquivo POM .

    <build>
      <plugins>
       <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <systemPropertyVariables>
              <PropertyName1>PropertyValue1</PropertyName1>                                                          
              <PropertyName2>PropertyValue2</PropertyName2>
          </systemPropertyVariables>
          <environmentVariables>
            <EnvironmentVariable1>EnvironmentVariableValue1</EnvironmentVariable1>
            <EnvironmentVariable2>EnvironmentVariableValue2</EnvironmentVariable2>
          </environmentVariables>
        </configuration>
      </plugin>
    </plugins>
  </build>

Após essa alteração, executei os Casos de Teste novamente e, de repente, todos funcionaram conforme o esperado. Para obter informações do leitor, eu explorei essa abordagem em Maven 3.x , então eu não tenho idéia sobre Maven 2.x .

RLD
fonte
2
Essa solução é a melhor e deve ser a aceita, porque você não precisará de nada adicional como uma lib. Maven sozinho é bastante útil. Obrigado @RLD
Semo
No entanto, o @Semo exige maven, que é um requisito muito maior do que usar uma lib. Ele combina o Junit Test com o pom, e agora o teste sempre precisa ser executado a partir do mvn, em vez de executá-lo diretamente no IDE da maneira usual.
Chirlo 29/11/19
@Chirlo, depende do que você quer que seu programa vincule. Usando o Maven, você pode configurar em um só lugar e escrever um código claro e conciso. Se você usa a biblioteca, precisa escrever o código em vários locais. Em relação ao seu ponto de execução de JUnits, é possível executar JUnits a partir do IDE como o Eclipse, mesmo se você usar o Maven.
RLD
@RLD, a única maneira que conheço no Eclipse seria executá-lo como uma configuração de execução 'Maven', em vez de um Junit, que é muito mais complicado e apenas possui saída de texto em vez da exibição normal do Junit. E eu não sigo exatamente o seu ponto de código puro e conciso e ter que escrever código em vários lugares. Para mim, ter dados de teste no pom que são usados ​​em um teste Junit é mais obscuro do que tê-los juntos. Eu estava nessa situação recentemente e acabei seguindo a abordagem de MathewFarwell, sem necessidade de bibliotecas / truques de pom e tudo está junto no mesmo teste.
Chirlo 04/12/19
1
Isso torna as variáveis ​​de ambiente codificadas e elas não podem ser alteradas de uma chamada de System.getenv para a seguinte. Corrigir?
Ian Stewart
12

Eu acho que a maneira mais limpa de fazer isso é com o Mockito.spy (). É um pouco mais leve do que criar uma classe separada para zombar e repassar.

Mova sua variável de ambiente buscando para outro método:

@VisibleForTesting
String getEnvironmentVariable(String envVar) {
    return System.getenv(envVar);
}

Agora, no seu teste de unidade, faça o seguinte:

@Test
public void test() {
    ClassToTest classToTest = new ClassToTest();
    ClassToTest classToTestSpy = Mockito.spy(classToTest);
    Mockito.when(classToTestSpy.getEnvironmentVariable("key")).thenReturn("value");
    // Now test the method that uses getEnvironmentVariable
    assertEquals("changedvalue", classToTestSpy.methodToTest());
}
pgkelley
fonte
12

Acho que isso não foi mencionado ainda, mas você também pode usar o Powermockito :

Dado:

package com.foo.service.impl;

public class FooServiceImpl {

    public void doSomeFooStuff() {
        System.getenv("FOO_VAR_1");
        System.getenv("FOO_VAR_2");
        System.getenv("FOO_VAR_3");

        // Do the other Foo stuff
    }
}

Você pode fazer o seguinte:

package com.foo.service.impl;

import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;

import org.junit.Beforea;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest(FooServiceImpl.class)
public class FooServiceImpTest {

    @InjectMocks
    private FooServiceImpl service;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mockStatic(System.class);  // Powermock can mock static and private methods

        when(System.getenv("FOO_VAR_1")).thenReturn("test-foo-var-1");
        when(System.getenv("FOO_VAR_2")).thenReturn("test-foo-var-2");
        when(System.getenv("FOO_VAR_3")).thenReturn("test-foo-var-3");
    }

    @Test
    public void testSomeFooStuff() {        
        // Test
        service.doSomeFooStuff();

        verifyStatic();
        System.getenv("FOO_VAR_1");
        verifyStatic();
        System.getenv("FOO_VAR_2");
        verifyStatic();
        System.getenv("FOO_VAR_3");
    }
}
Mangusta
fonte
8
when(System.getenv("FOO_VAR_1")).thenReturn("test-foo-var-1")causa org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'.erro
Andremoniy 24/10
10

Desacoplar o código Java da variável Environment, fornecendo um leitor de variáveis ​​mais abstrato que você percebe com um EnvironmentVariableReader do seu código para testar leituras.

Em seu teste, você pode fornecer uma implementação diferente do leitor de variáveis ​​que fornece seus valores de teste.

A injeção de dependência pode ajudar nisso.

Andrea Colleoni
fonte
4

Espero que o problema seja resolvido. Eu apenas pensei em dizer a minha solução.

Map<String, String> env = System.getenv();
    new MockUp<System>() {
        @Mock           
        public String getenv(String name) 
        {
            if (name.equalsIgnoreCase( "OUR_OWN_VARIABLE" )) {
                return "true";
            }
            return env.get(name);
        }
    };
Muthu
fonte
1
Você esqueceu de mencionar que está usando o JMockit. :) Independentemente disso, esta solução também funciona muito bem com o JUnit 5
Ryan J. McDonough
2

Embora eu ache que essa resposta é a melhor para projetos do Maven, ela também pode ser alcançada via reflect (testada em Java 8 ):

public class TestClass {
    private static final Map<String, String> DEFAULTS = new HashMap<>(System.getenv());
    private static Map<String, String> envMap;

    @Test
    public void aTest() {
        assertEquals("6", System.getenv("NUMBER_OF_PROCESSORS"));
        System.getenv().put("NUMBER_OF_PROCESSORS", "155");
        assertEquals("155", System.getenv("NUMBER_OF_PROCESSORS"));
    }

    @Test
    public void anotherTest() {
        assertEquals("6", System.getenv("NUMBER_OF_PROCESSORS"));
        System.getenv().put("NUMBER_OF_PROCESSORS", "77");
        assertEquals("77", System.getenv("NUMBER_OF_PROCESSORS"));
    }

    /*
     * Restore default variables for each test
     */
    @BeforeEach
    public void initEnvMap() {
        envMap.clear();
        envMap.putAll(DEFAULTS);
    }

    @BeforeAll
    public static void accessFields() throws Exception {
        envMap = new HashMap<>();
        Class<?> clazz = Class.forName("java.lang.ProcessEnvironment");
        Field theCaseInsensitiveEnvironmentField = clazz.getDeclaredField("theCaseInsensitiveEnvironment");
        Field theUnmodifiableEnvironmentField = clazz.getDeclaredField("theUnmodifiableEnvironment");
        removeStaticFinalAndSetValue(theCaseInsensitiveEnvironmentField, envMap);
        removeStaticFinalAndSetValue(theUnmodifiableEnvironmentField, envMap);
    }

    private static void removeStaticFinalAndSetValue(Field field, Object value) throws Exception {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, value);
    }
}
George Z.
fonte
Obrigado por isso! Minha versão do Java parece não ter theCaseInsensitiveEnvironmente, em vez disso, possui um campo theEnvironment, como o seguinte: `` `envMap = new HashMap <> (); Classe <?> Clazz = Class.forName ("java.lang.ProcessEnvironment"); Campo theEnvironmentField = clazz.getDeclaredField ("theEnvironment"); Campo theUnmodifiableEnvironmentField = clazz.getDeclaredField ("theUnmodifiableEnvironment"); removeStaticFinalAndSetValue (theEnvironmentField, envMap); removeStaticFinalAndSetValue (theUnmodifiableEnvironmentField, envMap); ``
Intenex
-2

Bem, você pode usar o método setup () para declarar os diferentes valores do seu ambiente. variáveis ​​em constantes. Em seguida, use essas constantes nos métodos de teste usados ​​para testar o cenário diferente.

Cygnusx1
fonte
-2

Se você deseja recuperar informações sobre a variável de ambiente em Java, você pode chamar o método: System.getenv();. Como as propriedades, esse método retorna um Mapa contendo os nomes das variáveis ​​como chaves e os valores das variáveis ​​como valores do mapa. Aqui está um exemplo :

    import java.util.Map;

public class EnvMap {
    public static void main (String[] args) {
        Map<String, String> env = System.getenv();
        for (String envName : env.keySet()) {
            System.out.format("%s=%s%n", envName, env.get(envName));
        }
    }
}

O método getEnv()também pode receber um argumento. Por exemplo :

String myvalue = System.getEnv("MY_VARIABLE");

Para testar, eu faria algo assim:

public class Environment {
    public static String getVariable(String variable) {
       return  System.getenv(variable);
}

@Test
 public class EnvVariableTest {

     @Test testVariable1(){
         String value = Environment.getVariable("MY_VARIABLE1");
         doSometest(value); 
     }

    @Test testVariable2(){
       String value2 = Environment.getVariable("MY_VARIABLE2");
       doSometest(value); 
     }   
 }
Dimitri
fonte
1
O ponto principal não é não acessar as variáveis env do teste JUnit
Tanmoy Bhattacharjee
-2

Uso System.getEnv () para obter o mapa e mantenho-o como um campo, para que eu possa zombar dele:

public class AAA {

    Map<String, String> environmentVars; 

    public String readEnvironmentVar(String varName) {
        if (environmentVars==null) environmentVars = System.getenv();   
        return environmentVars.get(varName);
    }
}



public class AAATest {

         @Test
         public void test() {
              aaa.environmentVars = new HashMap<String,String>();
              aaa.environmentVars.put("NAME", "value");
              assertEquals("value",aaa.readEnvironmentVar("NAME"));
         }
}
ejaenv
fonte