在单例中测试双重检查锁定机制,以获得100%的代码覆盖率

时间:2017-08-22 15:04:21

标签: java unit-testing junit singleton

我正在努力实现100%的代码覆盖率,试图在单例中为双重检查锁定机制编写单元测试。

if (test == null) {
    synchronized (Test.class) {
        if (test == null) {
            test = injector.getInstance(Test.class);
        }
    }
    return test;
}

如何测试第二次空检查的场景?我试着读了很多,却找不到办法。

2 个答案:

答案 0 :(得分:0)

更新:在与@vampire讨论之后,我得出的结论是:

  

如果没有使用纯双重检查的锁定代码,则不可能

所以,这只有在您满足以下条件时才能实现:

  • 通过在外部ifsynchronized关键字之间添加可以模拟的东西来插入代码(当然,这违背了双重检查锁定习惯用法的目的,即最大限度地减少外部线程之间的线程干扰机会) if检查和synchronized阻止)
  • 可以模拟您的注射器和
  • 有一种方法可以将test引用泄漏到同步块之外

示例

再次检查锁定代码:

private volatile TestObject testObject;

private AtomicBoolean state = new AtomicBoolean(false);

public void ensureTestObject() {
  if (testObject == null) {
    injector.tick();
    synchronized (TestObject.class) {
      if (testObject == null) {
        testObject = injector.getInstance(TestObject.class);
      } else {
        state.set(true);
      }
    }
  }
}

// @VisibleForTesting
void setTestObject(TestObject testObject) {
  this.testObject = testObject;
}

public boolean failedInnerIf() {
  return state.get();
}

测试内部if检查失败:

@Test
public void shouldFailInnerNullCheck() throws Exception {
  for (int i = 0; i < 10000; i++) {
    // given
    CountDownLatch insideSynchronizedBlock = new CountDownLatch(1);
    CountDownLatch insideOuterIf = new CountDownLatch(1);
    CountDownLatch continueTest = new CountDownLatch(1);
    Injector injector = mock(Injector.class);
    when(injector.getInstance(any()))
        .thenAnswer(invocation -> {
          insideSynchronizedBlock.countDown();
          try {
            continueTest.await();
          } catch (InterruptedException e) {
            fail();
          }
          return new TestObject();
        })
        .thenReturn(new TestObject());
    when(injector.tick())
        .thenReturn("tic")
        .thenAnswer(invocation -> {
          insideOuterIf.countDown();
          try {
            continueTest.await();
          } catch (InterruptedException e) {
            fail();
          }
          return "tac";
        })
        .thenReturn("toc");

    DoubleCheckedLocking doubleCheckedLocking = new DoubleCheckedLocking(injector);

    // when
    Thread thread1 = new Thread(() ->
        doubleCheckedLocking.ensureTestObject()
    );
    Thread thread2 = new Thread(() -> {
      try {
        insideOuterIf.await();
      } catch (InterruptedException e) {
        fail();
      }
      doubleCheckedLocking.setTestObject(new TestObject());
      continueTest.countDown();
    });
    Thread thread3 = new Thread(() -> {
      try {
        insideSynchronizedBlock.await();
      } catch (InterruptedException e) {
        fail();
      }
      doubleCheckedLocking.ensureTestObject();
    });

    thread1.start();
    thread2.start();
    thread3.start();

    thread1.join();
    thread2.join();
    thread3.join();

    // then
    assertTrue("Failed at try " + i, doubleCheckedLocking.failedInnerIf());
  }
}

它如何工作?

  • 我们并行启动3个线程
  • thread1块内的
  • synchronized
  • thread2testObject位于外部thread3支票中之后伪造if的并行初始化(这就是我们需要检测代码injector.tick()的地方) )
  • 'thread3'第二次调用ensureTestObject(),一旦if离开thread1块并且synchronized改变了测试,这将使内部thread2检查失败同时出现物体

答案 1 :(得分:0)

更新: 我从测试中删除了100%可靠变体的完全不必要的线程代码


无法对OP中使用synchronized块的经典双重检查锁定习惯用法进行100%可靠的测试,因为这样可能需要在外部ifsynchronized块或最新的同步块,以便您知道何时什么东西已经通过外部if并在synchronized块中等待。或者更准确地说,是在读取字段值之后并等待之前。

我的答案将被分割,我将首先展示一种方法,该方法如何以一种漂亮(但不是100%)可靠的方式测试该成语的“如果通过,如果通过,则通过内部”。然后,我将展示三个选项,说明如何稍微修改生产代码以使其易于测试以100%可靠。

另一个免责声明,我的示例代码将位于Spock中,因此将成为Groovy代码,因为恕我直言,即使您的生产代码是Java,尤其是在访问私有字段时,编写测试也要好得多或仅在Groovy中隐式工作的方法,只要它们不是最终的。但是,相应地,这些代码应该可以轻松地转换为普通的JUnit Java代码,尤其是在您的字段和方法不是私有的情况下。


因此,这里首先以一种非常可靠的方式来测试该成语,并内嵌一些变体和解释。

def 'field should not get changed once assigned even if outer check succeeds'() {
    given:
        def insideSynchronizedBlock = new CountDownLatch(1)
        def continueTest = new CountDownLatch(1)
        // this is the value the field is set to after the
        // test passed the outer if and before checking the
        // inner if
        def testFieldValue = new Object()

    when:
        // get the object the synchronized block is synchronizing on
        // this can be some class object, internal field or whatever,
        // in OP this is the Test class object so we are using it here too
        def initializationLock = Test

        // this thread will enter the synchronized block and then
        // wait for the continueTest latch and thus hinder
        // the main thread to enter it prematurely but wait at
        // the synchronized block after checking the outer if
        //
        // if inside the synchronized block content there is something
        // that can be mocked like some generator method call,
        // alternatively to manually synchronizing on the lock and setting
        // the field in question, the generator method could be mocked
        // to first block on the continueTest latch and then return testFieldValue
        def synchronizedBlocker = Thread.start {
            synchronized (initializationLock) {
                // signal that we now block the synchronized block
                insideSynchronizedBlock.countDown()

                // wait for the continueTest latch that should be triggered
                // after the outer if was passed so that now the field
                // gets assigned so that the inner if will fail
                //
                // as we are in a different thread, an InterruptedException
                // would just be swallowed, so repeat the waiting until the
                // latch actually reached count 0, alternatively you could
                // mark the test as failed in the catch block if your test
                // framework supports this from different threads, you might
                // also consider to have some maximum waiting time that is
                // checked if an interrupt happened
                while (continueTest.count != 0) {
                    try {
                        continueTest.await()
                    } catch (InterruptedException ignore) {
                        // ignore, await is retried if necessary
                    }
                }
                // as the outer if was passed, now set the field value
                // in the testee to the test field value, we called the
                // testee field test here as was in OP example
                // after this the synchronized block will be exited
                // and the main test code can continue and check the inner if
                //
                // in the mocking case mentioned above, here testFieldValue
                // would be returned by the mocked injector method
                testee.test = testFieldValue
            }
        }

        // wait for the synchronizedBlocker to block the synchronized block
        // here if an InterruptedException happens, the test will fail,
        // alternatively you could also here do some while-loop that retries
        // the waiting, also with an optional maximum waiting time
        insideSynchronizedBlock.await()

        // this thread will trigger the continueTest latch and thus
        // implicitly cause the field to be set and the synchronized
        // block to be exited
        //
        // this is the problematic part why this solution is not 100% reliable
        // because this thread should start only after the main test code has
        // checked the outer if, otherwise the field value might be set before
        // the outer if is checked and so the outer if will already fail
        //
        // the test here will not detect this but stay green, the only way this
        // would be observed is because branch coverage would drop by one if this
        // happened; unfortunately that this happens is not too abstract but a
        // real "problem", so if you want to maintain 100% branch coverage, you
        // have to take additional measures, like for example these:
        //
        // - minimize the delay between the starting of this thread and the main
        //   test code actually passing the outer if check to increase probability
        //   that the if check was passed before this thread gets scheduled
        //   if you for example call the method in question via reflection because
        //   it is private and you don't want ot open it up for testing, this is
        //   slower than usual and you should find and get the method reference
        //   before starting this thread and you might also call it before on some
        //   dummy instance, so that the JIT compiler has a chance to optimize the
        //   access and so on
        //
        //   just do anything you can to minimize the time between starting this
        //   thread and passing the outer if in the main test code, but whatever
        //   you do, it is most probably not enough to have halfway reliable 100%
        //   branch coverage so you might need to take additional measures
        //
        // - add a yield() to this thread before calling countDown on the
        //   continueTest latch to give the main thread a chance to pass the outer
        //   if, if that is enough in your situation
        //
        //   if not, instead add a sleep() to this thread at the same place to
        //   give the main thread more time to pass the outer if, how long this
        //   sleep must be cannot be said and you just have to test what value
        //   you need to have halfway reliable results, it might already be
        //   enough to sleep for 50 ms or even less, just try it out
        //
        // - repeat the test X times so that at least with one of the tries
        //   the intended branch was hit and thus branch coverage will stay at
        //   100%, how big X is again has to be determined from case to case
        //   by trial and error or by wasting time by setting a very high value
        //
        //   in case of Spock being used the repeating can simply be achieved
        //   by adding a `where: i << (1..1000)` after the cleanup block to repeat
        //   the test a thousand times, for more information read about data
        //   driven testing in Spock, the data variable is simply ignored here
        //   and only cares for the identically repeated execution of the test
        def synchronizedUnBlocker = Thread.start {
            continueTest.countDown()
        }

    then:
        // here hopefully the outer if is passed before the synchronizedUnBlocker
        // is scheduled and then we wait at the synchronized block
        // then the synchronizedUnBlocker is scheduled and counts down the latch
        // the synchronizedBlocker is waiting for which in turn will set the
        // field value to testFieldValue
        // then the synchronizedBlocker exist and thus unblocks the synchronized
        // block, allowing the inner if to be tested here which now should fail
        // and the field value staying at testFieldValue
        // that the testFieldValue stays set is verified here, but this would also
        // be green if the outer if failed already because the synchronizedUnBlocker
        // was scheduled before the outer if was passed
        testee.getCommandPrefix(message) == testFieldValue

    cleanup:
        // wait for the helper threads to die,
        // this is not necessarily required they will die anyway
        synchronizedBlocker?.join()
        synchronizedUnBlocker?.join()
}

如前所述,(据我所知)100%可靠地测试双重检查锁定的唯一方法是修改生产代码,使其能够在两个if检查之间并且在主线程之前进行钩挂某些监视器上的块。更准确地是,在读取外部if的字段值之后,在线程阻塞之前。

这可以通过多种方式实现,我在这里展示了其中的一些方式,因此您可以选择最喜欢的一种方式,也可以根据这些方式开发自己的方式。这些替代方案未经过性能测试,我不会对它们做任何性能说明,我只是说这些替代方案仍然可以正常工作,并且可以100%可靠地测试,覆盖范围为100%。如果您熟悉微基准测试,请使用这些方法进行一些适当的微基准测试性能测试,并希望我将它们添加到答案中,只是告诉我,然后添加信息。

这是我将要修改的原始的双重检查锁定Java方法:

public class Test {
    private final Object fieldInitializationLock = new Object();
    private volatile String field;
    public String getField() {
        String field = this.field;
        if (field == null) {
            synchronized (fieldInitializationLock) {
                field = this.field;
                if (field == null) {
                    field = "";
                    this.field = field;
                }
            }
        }
        return field;
    }
}

这里是根据我上面的解释进行的相应测试:

import spock.lang.Specification

import java.util.concurrent.CountDownLatch

class DoubleCheckedLockingTest extends Specification {
    def 'field should not get changed once assigned even if outer check succeeds'() {
        given:
            def testee = new Test()
            def insideSynchronizedBlock = new CountDownLatch(1)
            def continueTest = new CountDownLatch(1)
            def testFieldValue = new String()

        when:
            def synchronizedBlocker = Thread.start {
                synchronized (testee.fieldInitializationLock) {
                    insideSynchronizedBlock.countDown()
                    while (continueTest.count != 0) {
                        try {
                            continueTest.await()
                        } catch (InterruptedException ignore) {
                            // ignore, await is retried if necessary
                        }
                    }
                    testee.field = testFieldValue
                }
            }

            insideSynchronizedBlock.await()
            def synchronizedUnBlocker = Thread.start {
                continueTest.countDown()
            }

        then:
            testee.getField().is(testFieldValue)

        cleanup:
            synchronizedBlocker?.join()
            synchronizedUnBlocker?.join()
    }
}

第一个版本可能也是我最喜欢的版本。

它使用更高级别的同步工具ReadWriteLock进行双重检查锁定,而不是在某些监视器对象上进行易失性字段和同步块的锁定。请注意,对于此变体,该字段不再需要volatile,我们也不需要防止对volatile字段进行多次慢速访问的本地字段。

这是我的最爱,因为像所有其他100%可靠的解决方案一样,它需要更改生产来源,但是仍然只有生产代码。只是使用其他更高级别的工具来实现该逻辑,并且这样做突然变得可以正确测试。其他方法可能看起来更简单并且需要的更改更少,但是它们都引入了专门用于使该成语可测试且没有任何生产价值的代码。

因此,这里是修改后的Java源代码:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();
    private final Lock writeLock = readWriteLock.writeLock();
    private String field;
    public String getField() {
        readLock.lock();
        try {
            if (field == null) {
                readLock.unlock();
                try {
                    writeLock.lock();
                    try {
                        if (field == null) {
                            field = "";
                        }
                    } finally {
                        writeLock.unlock();
                    }
                } finally {
                    readLock.lock();
                }
            }
            return field;
        } finally {
            readLock.unlock();
        }
    }
}

这里是相应的测试。与原始测试的不同之处在于,它可以100%可靠地工作并且不需要额外的线程。我们需要设置一个最终变量readLock,但是由于反射,它可以正常工作,并且对于测试代码恕我直言是可以的。如果这是不可能的或不希望的,我们还可以使用诸如PowerMock之类的模拟工具,该工具可以模拟new ReentrantReadWriteLock()调用。

我们只需装饰writeLock.lock()调用即可。在进行实际锁定之前,我们先设置有问题的字段,以使内部if失败,而这正是我们想要实现的目标。

import spock.lang.Specification

class DoubleCheckedLockingTest extends Specification {
    def 'field should not get changed once assigned even if outer check succeeds'() {
        given:
            def testee = new Test()
            def testFieldValue = new String()

        and:
            Test.getDeclaredField("writeLock")
                    .tap { it.accessible = true }
                    .set(testee, Spy(testee.writeLock))
            testee.writeLock.lock() >> {
                testee.field = testFieldValue
                callRealMethod()
            }

        expect:
            testee.getField().is(testFieldValue)
    }
}

所有其他变体在检查if条件和获取监视器之间人为地引入了一些内容,以便可以向同步阻止程序发信号,以继续使用先前的方法已经自动执行的操作。


以下变体引入了一些东西,可以用来设置有问题的字段,并且在生产时不执行任何操作。在此示例中,我在hashCode()上使用了Object,并在测试中将其替换为存根,但是您可以使用任何其他变体,例如拥有不执行任何操作的类,然后创建一个新对象,然后使用PowerMock或类似工具的乐器。唯一重要的一点是,您可以在测试时对它进行仪表化,在生产时它什么也不做,或者做得尽可能少,并且不会被编译器优化。

以下是Java代码,其中仅添加了带有testHook的两行:

public class Test {
    private final Object fieldInitializationLock = new Object();
    private volatile String field;
    private Object testHook;
    public String getField() {
        String field = this.field;
        if (field == null) {
            testHook.hashCode();
            synchronized (fieldInitializationLock) {
                field = this.field;
                if (field == null) {
                    field = "";
                    this.field = field;
                }
            }
        }
        return field;
    }
}

以及相应的测试代码,其中testHook被替换为已存好的存根:

import spock.lang.Specification

class DoubleCheckedLockingTest extends Specification {
    def 'field should not get changed once assigned even if outer check succeeds'() {
        given:
            def testee = new Test()
            def testFieldValue = new String()

        and:
            testee.testHook = Stub(Object)
            testee.testHook.hashCode() >> {
                testee.field = testFieldValue
                1
            }

        expect:
            testee.getField().is(testFieldValue)
    }
}

以下变体用if代替了Objects.isNull(...)中的等于校验,然后使用PowerMock调用的工具。从生产代码的角度来看,此变体也不太差,但是当前的缺点是必须使用PowerMock,并且PowerMock不能与默认和推荐的JaCoCo即时检测工具协调一致,因此,经过测试的类将具有0%的测试覆盖率,或者必须使用JaCoCo离线检测进行检测。我只是在这里提到它,尽管它很容易或相反,只是因为它可以正确完成。

Java类:

import static java.util.Objects.isNull;

public class Test {
    private final Object fieldInitializationLock = new Object();
    private volatile String field;
    public String getField() {
        String field = this.field;
        if (isNull(field)) {
            synchronized (fieldInitializationLock) {
                field = this.field;
                if (field == null) {
                    field = "";
                    this.field = field;
                }
            }
        }
        return field;
    }
}

和相应的测试:

import groovy.transform.CompileStatic
import org.junit.runner.RunWith
import org.mockito.stubbing.Answer
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import spock.lang.Specification

import static org.mockito.ArgumentMatchers.isNull
import static org.mockito.Mockito.when
import static org.mockito.Mockito.withSettings
import static org.powermock.api.mockito.PowerMockito.mockStatic

@RunWith(PowerMockRunner)
@PrepareForTest(Test)
class DoubleCheckedLockingTest extends Specification {
    // this helper is necessary, or PowerMock cannot properly mock
    // the system class, as it instruments the test class to intercept
    // and record the call to the system class
    // without compile static the dynamic Groovy features prevent this
    @CompileStatic
    def staticallyCompiledHelper(Answer answer) {
        when(Objects.isNull(isNull())).thenAnswer(answer)
    }

    def 'field should not get changed once assigned even if outer check succeeds'() {
        given:
            def testee = new Test()
            def testFieldValue = new String()

        and:
            mockStatic(Objects, withSettings().stubOnly())
            staticallyCompiledHelper {
                // as the field is value is already read
                // we can already trigger the continueTest latch
                // first and do the null-check second
                testee.field = testFieldValue
                it.callRealMethod()
            }

        expect:
            testee.getField().is(testFieldValue)
    }
}