Jak przetestować kod zależny od zmiennych środowiskowych za pomocą JUnit?

139

Mam fragment kodu Java, który używa zmiennej środowiskowej, a zachowanie kodu zależy od wartości tej zmiennej. Chciałbym przetestować ten kod z różnymi wartościami zmiennej środowiskowej. Jak mogę to zrobić w JUnit?

Widziałem ogólnie sposoby ustawiania zmiennych środowiskowych w Javie , ale bardziej interesuje mnie aspekt testów jednostkowych, zwłaszcza biorąc pod uwagę, że testy nie powinny ze sobą kolidować.

vitaut
źródło
Ponieważ jest to przeznaczone do testowania, reguła testów jednostkowych Reguł systemowych może być obecnie najlepszą odpowiedzią.
Atifm
3
Tylko dla zainteresowanych tym samym pytaniem podczas korzystania z JUnit 5: stackoverflow.com/questions/46846503/…
Felipe Martins Melo

Odpowiedzi:

199

Biblioteka Zasady systemu zapewnia JUnit regułę do ustawiania zmiennych środowiskowych.

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"));
  }
}

Zastrzeżenie: jestem autorem reguł systemowych.

Stefan Birkner
źródło
1
Używam tego jako @ClassRule, czy muszę go zresetować lub wyczyścić po użyciu, jeśli tak, to w jaki sposób?
Mritunjay
Nie musisz. Oryginalne zmienne środowiskowe są automatycznie resetowane przez regułę po wykonaniu wszystkich testów w klasie.
Stefan Birkner
To podejście działa tylko w przypadku JUnit 4 lub nowszej wersji. Niezalecane dla JUnit 3 lub niższej wersji lub jeśli łączysz JUnit 4 i JUnit 3.
RLD
2
import org.junit.contrib.java.lang.system.EnvironmentVariables;Będziesz musiał dodać zależność com.github.stefanbirkner:system-rulesw swoim projekcie. Jest dostępny w MavenCentral.
Jean Bob
2
Oto instrukcje dodawania zależności: stefanbirkner.github.io/system-rules/download.html
Guilherme Garnier
77

Typowym rozwiązaniem jest utworzenie klasy, która zarządza dostępem do tej zmiennej środowiskowej, którą można następnie symulować w klasie testowej.

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());
    }
}

Testowana klasa pobiera następnie zmienną środowiskową przy użyciu klasy Environment, a nie bezpośrednio z System.getenv ().

Matthew Farwell
źródło
1
Wiem, że to pytanie jest stare, ale chciałem powiedzieć, że to jest poprawna odpowiedź. Przyjęta odpowiedź zachęca do słabego projektu z ukrytą zależnością od Systemu, podczas gdy ta odpowiedź zachęca do właściwego projektu, traktując System jako kolejną zależność, którą należy wstrzyknąć.
Andrew
30

W podobnej sytuacji, jak ta, w której musiałem napisać przypadek testowy zależny od zmiennej środowiskowej , próbowałem wykonać następujące czynności:

  1. Wybrałem Zasady Systemowe, zgodnie z sugestią Stefana Birknera . Jego użycie było proste. Ale wcześniej niż później stwierdziłem, że zachowanie jest nieobliczalne. Za jednym razem działa, w następnym zawodzi. Zbadałem i stwierdziłem, że reguły systemowe dobrze działają z JUnit 4 lub nowszą wersją. Ale w moich przypadkach używałem kilku słoików, które były zależne od JUnit 3 . Więc pominąłem Reguły systemowe . Więcej na ten temat można znaleźć tutaj Adnotacja @Rule nie działa podczas korzystania z TestSuite w JUnit .
  2. Następnie próbowałem stworzyć zmienną środowiskową za pomocą klasy Process Builder dostarczonej przez Javę . Tutaj za pomocą kodu Java możemy stworzyć zmienną środowiskową, ale musisz znać nazwę procesu lub programu, których nie znałem. Tworzy również zmienną środowiskową dla procesu potomnego, a nie dla procesu głównego.

Zmarnowałem dzień stosując powyższe dwa podejścia, ale bezskutecznie. Wtedy Maven przyszedł mi na ratunek. Możemy ustawić zmienne środowiskowe lub właściwości systemu za pomocą pliku POM Maven , który moim zdaniem jest najlepszym sposobem na wykonanie testów jednostkowych dla projektu opartego na Maven . Poniżej wpis, który zrobiłem w pliku 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>

Po tej zmianie ponownie uruchomiłem Test Cases i nagle wszystko działało zgodnie z oczekiwaniami. Dla informacji czytającego, ja zbadałem to podejście w Maven 3.x , więc nie mam pojęcia o Maven 2.x .

RLD
źródło
2
To rozwiązanie jest najlepsze i powinno być akceptowane, ponieważ nie będziesz potrzebować niczego dodatkowego, jak lib. Sam Maven jest wystarczająco przydatny. Dziękuję @RLD
Semo
@Semo wymaga jednak mavena, co jest znacznie większym wymaganiem niż używanie lib. Łączy test Junit z pom, a test musi być teraz zawsze wykonywany z mvn, zamiast uruchamiać go bezpośrednio w IDE w zwykły sposób.
Chirlo
@Chirlo, to zależy od tego, z czym chcesz powiązać program. Korzystając z Mavena, możesz skonfigurować w jednym miejscu i napisać zgrabny i zwięzły kod. Jeśli korzystasz z biblioteki, musisz pisać kod w wielu miejscach. Jeśli chodzi o twój punkt uruchamiania JUnits, możesz uruchamiać JUnits z IDE, takich jak Eclipse, nawet jeśli używasz Maven.
RLD
@RLD, jedynym sposobem, jaki znam w Eclipse, byłoby uruchomienie go jako konfiguracji uruchamiania „Maven” zamiast Junita, który jest znacznie bardziej uciążliwy i ma tylko wyjście tekstowe zamiast normalnego widoku Junit. I nie do końca podążam za twoim punktem zgrabnego i zwięzłego kodu oraz konieczności pisania kodu w wielu miejscach. Dla mnie posiadanie danych testowych w pompie, które są następnie używane w teście Junit, jest bardziej niejasne niż posiadanie ich razem. Niedawno byłem w takiej sytuacji i ostatecznie podążyłem za podejściem MathewFarwella, nie ma potrzeby korzystania z bibliotek / sztuczek pomorskich i wszystko jest razem w tym samym teście.
Chirlo
1
To sprawia, że ​​zmienne środowiskowe są zakodowane na stałe i nie można ich zmienić od jednego wywołania System.getenv do następnego. Poprawny?
Ian Stewart
12

Myślę, że najczystszym sposobem na to jest użycie Mockito.spy (). Jest to trochę lżejsze niż tworzenie oddzielnej klasy do kpiny i przekazywania.

Przenieś pobieranie zmiennej środowiskowej do innej metody:

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

Teraz w swoim teście jednostkowym zrób to:

@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
źródło
12

Wydaje mi się, że nie było to jeszcze wspominane, ale możesz też użyć Powermockito :

Dany:

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
    }
}

Możesz wykonać następujące czynności:

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
źródło
8
when(System.getenv("FOO_VAR_1")).thenReturn("test-foo-var-1")powoduje org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'.błąd
Andremoniy
10

Oddziel kod Java od zmiennej środowiskowej, zapewniając bardziej abstrakcyjny czytnik zmiennych, który można zrealizować za pomocą EnvironmentVariableReader kodu do testowania odczytów.

Następnie w swoim teście możesz podać inną implementację czytnika zmiennych, który dostarcza wartości testowe.

Pomóc w tym może zastrzyk zależności.

Andrea Colleoni
źródło
4

Mam nadzieję, że problem został rozwiązany. Po prostu pomyślałem, żeby powiedzieć moje rozwiązanie.

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
źródło
1
Zapomniałeś wspomnieć, że używasz JMockit. :) Niezależnie od tego, to rozwiązanie działa również świetnie z JUnit 5
Ryan J. McDonough.
1

Chociaż uważam, że ta odpowiedź jest najlepsza dla projektów Maven, można ją również osiągnąć za pomocą refleksu (przetestowanego w Javie 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.
źródło
Dzięki za to! Wydaje się, że moja wersja Javy nie ma theCaseInsensitiveEnvironmenti zamiast tego ma pole theEnvironment, takie jak następujące: `` envMap = new HashMap <> (); Class <?> Clazz = Class.forName ("java.lang.ProcessEnvironment"); Pole theEnvironmentField = clazz.getDeclaredField ("theEnvironmentField"); Pole theUnmodifiableEnvironmentField = clazz.getDeclaredField ("theUnmodifiableEnvironment"); removeStaticFinalAndSetValue (theEnvironmentField, envMap); removeStaticFinalAndSetValue (theUnmodifiableEnvironmentField, envMap); ``
Intenex
-2

Cóż, możesz użyć metody setup (), aby zadeklarować różne wartości twojego env. zmienne w stałych. Następnie użyj tych stałych w metodach testowych używanych do testowania różnych scenariuszy.

Cygnusx1
źródło
-2

Jeśli chcesz, aby pobrać informacje o zmiennej środowiskowej w Javie, można wywołać metodę: System.getenv();. Jako właściwości ta metoda zwraca Map zawierającą nazwy zmiennych jako klucze, a wartości zmiennych jako wartości map. Oto przykład :

    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));
        }
    }
}

Metoda getEnv()może również pobierać argument. Na przykład :

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

Do testów zrobiłbym coś takiego:

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
źródło
1
Nie chodzi o to, aby nie uzyskać dostępu do zmiennych env z testu
junit
-2

Używam System.getEnv (), aby pobrać mapę i zachowuję jako pole, więc mogę z niej kpić:

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
źródło