【问题标题】:Mocking a singleton with mockito用 mockito 模拟单例
【发布时间】:2016-08-12 09:23:50
【问题描述】:

我需要测试一些在方法调用中使用单例的遗留代码。测试的目的是确保类 sunder 测试调用单例方法。 我在 SO 上看到过类似的问题,但所有答案都需要其他依赖项(不同的测试框架)——不幸的是,我仅限于使用 Mockito 和 JUnit,但使用如此流行的框架应该完全可以做到这一点。

单身人士:

public class FormatterService {

    private static FormatterService INSTANCE;

    private FormatterService() {
    }

    public static FormatterService getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new FormatterService();
        }
        return INSTANCE;
    }

    public String formatTachoIcon() {
        return "URL";
    }

}

被测类:

public class DriverSnapshotHandler {

    public String getImageURL() {
        return FormatterService.getInstance().formatTachoIcon();
    }

}

单元测试:

public class TestDriverSnapshotHandler {

    private FormatterService formatter;

    @Before
    public void setUp() {

        formatter = mock(FormatterService.class);

        when(FormatterService.getInstance()).thenReturn(formatter);

        when(formatter.formatTachoIcon()).thenReturn("MockedURL");

    }

    @Test
    public void testFormatterServiceIsCalled() {

        DriverSnapshotHandler handler = new DriverSnapshotHandler();
        handler.getImageURL();

        verify(formatter, atLeastOnce()).formatTachoIcon();

    }

}

这个想法是配置可怕的单例的预期行为,因为被测试的类将调用它的 getInstance 和 formatTachoIcon 方法。不幸的是,这失败并显示错误消息:

when() requires an argument which has to be 'a method call on a mock'.

【问题讨论】:

  • 你不能在 Mockito 中做到这一点,除非你重构你的类之一,而不使用 PowerMock。但我不确定你为什么要这样做。您正在对一种方法进行单元测试,只有一行,没有内部逻辑。这不能失败。将您的测试工作花在其他地方。
  • "测试的目的是确保类 sunder 测试调用单例方法。"任何好的测试用例都不应该以这种事情为目的。相反,旨在测试一些有意义的业务功能。模拟一个依赖并验证一个方法是否被调用不一定是错误的,但应该只在需要的时候做。
  • 所有这些方法只会让人头疼......它们适用于静态方法,但如果它是实例的方法并且它引用一个类变量,它会得到空指针对吗?但 powerMockito 是一种方式。取而代之的是做我们在过去所做的事情,让我们的 singleTon 实现一个接口。然后在您的测试中使用其中包含所有存根的接口。参考:stackoverflow.com/a/17325647/835883
  • @fbielejec mock 方法里面是什么?

标签: java unit-testing junit mocking mockito


【解决方案1】:

您的要求是不可能的,因为您的旧代码依赖于静态方法 getInstance() 并且 Mockito 不允许模拟静态方法,因此以下行不起作用

when(FormatterService.getInstance()).thenReturn(formatter);

有两种方法可以解决这个问题:

  1. 使用不同的模拟工具,例如 PowerMock,它允许模拟静态方法。

  2. 重构您的代码,以便您不依赖静态方法。我能想到的实现这一点的侵入性最小的方法是向DriverSnapshotHandler 添加一个构造函数,该构造函数注入一个FormatterService 依赖项。此构造函数将仅在测试中使用,您的生产代码将继续使用真正的单例实例。

    public static class DriverSnapshotHandler {
    
        private final FormatterService formatter;
    
        //used in production code
        public DriverSnapshotHandler() {
            this(FormatterService.getInstance());
        }
    
        //used for tests
        DriverSnapshotHandler(FormatterService formatter) {
            this.formatter = formatter;
        }
    
        public String getImageURL() {
            return formatter.formatTachoIcon();
        }
    }
    

那么,你的测试应该是这样的:

FormatterService formatter = mock(FormatterService.class);
when(formatter.formatTachoIcon()).thenReturn("MockedURL");
DriverSnapshotHandler handler = new DriverSnapshotHandler(formatter);
handler.getImageURL();
verify(formatter, atLeastOnce()).formatTachoIcon();

【讨论】:

  • 第三种方法是认识到(可能)可以在不模拟单例的情况下编写更好的测试。在这种情况下我们无法确定,因为它是XY question
  • 仅出于测试目的添加代码是一种好习惯吗? (DriverSnapshotHandler)
【解决方案2】:

我认为这是可能的。查看示例how to test a singleton

测试前:

@Before
public void setUp() {
    formatter = mock(FormatterService.class);
    setMock(formatter);
    when(formatter.formatTachoIcon()).thenReturn(MOCKED_URL);
}

private void setMock(FormatterService mock) {
    try {
        Field instance = FormatterService.class.getDeclaredField("instance");
        instance.setAccessible(true);
        instance.set(instance, mock);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

测试后 - 清理类很重要,因为其他测试会与模拟实例混淆。

@After
public void resetSingleton() throws Exception {
   Field instance = FormatterService.class.getDeclaredField("instance");
   instance.setAccessible(true);
   instance.set(null, null);
}

测试:

@Test
public void testFormatterServiceIsCalled() {
    DriverSnapshotHandler handler = new DriverSnapshotHandler();
    String url = handler.getImageURL();

    verify(formatter, atLeastOnce()).formatTachoIcon();
    assertEquals(MOCKED_URL, url);
}

【讨论】:

    【解决方案3】:

    我只想从 noscreenname 完成解决方案。解决方案是使用 PowerMockito。因为 PowerMockito 可以做类似 Mockito 的事情,所以 sometimes 你可以使用 PowerMockito 。

    示例代码在这里:

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.powermock.api.mockito.PowerMockito;
    import org.powermock.core.classloader.annotations.PrepareForTest;
    import org.powermock.modules.junit4.PowerMockRunner;
    
    
    import java.lang.reflect.Field;
    
    import static org.powermock.api.mockito.PowerMockito.mock;
    import static org.powermock.api.mockito.PowerMockito.when;
    
    @RunWith(PowerMockRunner.class)
    @PrepareForTest({Singleton.class})
    public class SingletonTest {
    
        @Test
        public void test_1() {
            // create a mock singleton and change
            Singleton mock = mock(Singleton.class);
            when(mock.dosth()).thenReturn("succeeded");
            System.out.println(mock.dosth());
    
            // insert that singleton into Singleton.getInstance()
            PowerMockito.mockStatic(Singleton.class);
            when(Singleton.getInstance()).thenReturn(mock);
            System.out.println("result:" + Singleton.getInstance().dosth());
        }
    
    }
    

    单例类:

    public class Singleton {
    
        private static Singleton INSTANCE;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new Singleton();
            }
            return INSTANCE;
        }
    
        public String dosth() {
            return "failed";
        }
    
    }
    

    这是我的 Gradle:

    /*
    *  version compatibility see: https://github.com/powermock/powermock/wiki/mockito
    *
    * */
    
    def powermock='2.0.2'
    def mockito='2.8.9'
    ...
    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.12'
    
        /** mock **/
        testCompile group: 'org.mockito', name: 'mockito-core', version: "${mockito}"
    
        testCompile "org.powermock:powermock-core:${powermock}"
        testCompile "org.powermock:powermock-module-junit4:${powermock}"
        testCompile "org.powermock:powermock-api-mockito2:${powermock}"
        /**End of power mock **/
    
    }
    

    【讨论】:

      【解决方案4】:

      您的 getInstance 方法是静态的,因此无法使用 mockito 进行模拟。 http://cube-drone.com/media/optimized/172.png。您可能想使用PowerMockito 来执行此操作。虽然我不建议这样做。我会通过依赖注入来测试 DriverSnapshotHandler:

      public class DriverSnapshotHandler {
      
          private FormatterService formatterService;
      
          public DriverSnapshotHandler(FormatterService formatterService) {
              this.formatterService = formatterService;
          }
      
          public String getImageURL() {
              return formatterService.formatTachoIcon();
          }
      
      }
      

      单元测试:

      public class TestDriverSnapshotHandler {
      
          private FormatterService formatter;
      
          @Before
          public void setUp() {
      
              formatter = mock(FormatterService.class);
      
              when(formatter.formatTachoIcon()).thenReturn("MockedURL");
      
          }
      
          @Test
          public void testFormatterServiceIsCalled() {
      
              DriverSnapshotHandler handler = new DriverSnapshotHandler(formatter);
              handler.getImageURL();
      
              verify(formatter, times(1)).formatTachoIcon();
      
          }
      
      }
      

      您可能希望在 @After 方法中将模拟设置为 null。 恕我直言,这是更清洁的解决方案。

      【讨论】:

      • 你需要删除when(FormatterService.getInstance()).thenReturn(formatter)这行,否则测试不会运行
      【解决方案5】:

      如果它可以帮助某人 这是我测试单例类的方法 你只需要模拟你所有的单例类,然后使用 doCallRealMethod 来真正调用你想要测试的方法。

      SingletonClass.java:

      class SingletonClass {
      
          private static SingletonClass sInstance;
      
          private SingletonClass() {
              //do somethings
          }
      
          public static synchronized SingletonClass getInstance() {
              if (sInstance == null) {
                  sInstance = new SingletonClass();
              }
      
              return sInstance;
          }
      
          public boolean methodToTest() {
              return true;
          }
      }
      

      SingletonClassTest.java:

      import org.junit.Before;
      import org.junit.Test;
      import org.mockito.Mockito;
      
      import static org.junit.Assert.assertTrue;
      import static org.mockito.Mockito.mock;
      
      public class SingletonClassTest {
      
          private SingletonClass singletonObject;
      
          @Before
          public void setUp() throws Exception {
              singletonObject = mock(SingletonClass.class);
      
              Mockito.doCallRealMethod().when(singletonObject).methodToTest();
          }
      
          @Test
          public void testMethodToTest() {
              assertTrue(singletonObject.methodToTest());
          }
      }
      

      【讨论】:

        【解决方案6】:

        我有一个使用反射模拟单例类的解决方法。在设置测试时,您可能会考虑执行以下操作。

        @Mock 
        private MySingletonClass mockSingleton;
        
        private MySingletonClass originalSingleton;
        
        @Before 
        public void setup() {
            originalSingleton = MySingletonClass.getInstance();
            when(mockSingleton.getSomething()).thenReturn("Something"); // Use the mock to return some mock value for testing
        
            // Now set the instance with your mockSingleton using reflection 
            ReflectionHelpers.setStaticField(MySingletonClass.class, "instance", mockSingleton);
        }
        
        @After
        public void tearDown() {
            // Reset the singleton object when the test is complete using reflection again
            ReflectionHelpers.setStaticField(MySingletonClass.class, "instance", null);
        }
        
        @Test
        public void someTest() {
            // verify something here inside your test function.
        }
        

        ReflectionHelpers 由 Android 中的Robolectric 提供。但是,您始终可以编写自己的函数来帮助您。您可以check the question here 了解一下。

        【讨论】:

          【解决方案7】:

          作为 IMO 软件开发的初学者,在驱动程序/其他服务中注入单例类的依赖项是一个不错的选择。 因为我们可以控制类的单个实例的创建,并且仍然能够模拟静态方法(正如你可能已经猜到的,我脑子里有 util 服务)而不使用 PowerMock 之类的东西来模拟静态方法(IME有点痛) 我非常愿意从 SOLID良好的 OO 设计原则 角度听取有经验的人的意见。

          public class DriverSnapshotHandler {
              private FormatterService formatter;
              public DriverSnapshotHandler() {
                  this(FormatterService.getInstance());
              }
              public DriverSnapshotHandler (FormatterService formatterService){
                     this.formatter = formatterService;
              }
              public String getImageURL() {
                  return FormatterService.getInstance().formatTachoIcon();
              }
          }
          
          and then test using Mockito, something like this.
          
          @Test
          public void testGetUrl(){
            FormatterService formatter = mock(FormatterService.class);
            when(formatter.formatTachoIcon()).thenReturn("TestURL");
            DriverSnapshotHandler handler = new DriverSnapshotHandler(formatter);
            assertEquals(handler.getImageURL(), "TestUrl";
          }
          

          【讨论】:

            【解决方案8】:

            你可以使用powermock/反射来改变实例变量本身的值。

            FormatterService formatter = mock(FormatterService.class);
            when(formatter.formatTachoIcon()).thenReturn("MockedURL");
            
            // here use reflection or whitebox 
            
            Whitebox.setInternalState(FormatterService.class, "INSTANCE", formatter);
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2013-04-03
              • 1970-01-01
              • 2019-08-09
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多