[java] كيفية استخدام Junit لاختبار العمليات غير المتزامنة


Answers

بديل هو استخدام فئة CountDownLatch .

public class DatabaseTest {

    /**
     * Data limit
     */
    private static final int DATA_LIMIT = 5;

    /**
     * Countdown latch
     */
    private CountDownLatch lock = new CountDownLatch(1);

    /**
     * Received data
     */
    private List<Data> receiveddata;

    @Test
    public void testDataRetrieval() throws Exception {
        Database db = new MockDatabaseImpl();
        db.getData(DATA_LIMIT, new DataCallback() {
            @Override
            public void onSuccess(List<Data> data) {
                receiveddata = data;
                lock.countDown();
            }
        });

        lock.await(2000, TimeUnit.MILLISECONDS);

        assertNotNull(receiveddata);
        assertEquals(DATA_LIMIT, receiveddata.size());
    }
}

ملاحظة لا يمكنك فقط استخدام syncronized مع كائن عادي كقفل ، حيث يمكن أن يؤدي الاسترداد السريع إلى تحرير القفل قبل استدعاء أسلوب الانتظار في القفل. انظر this بلوق وظيفة من قبل جو فالنس.

EDIT إزالة الكتل المتزامنة حول CountDownLatch بفضل تعليقاتjtahlborn و @ Ring

Question

كيف يمكنك اختبار الطرق التي تطلق عمليات غير متزامنة مع Junit؟

لا أعرف كيف أجعل الاختبار الخاص بي ينتظر انتهاء العملية (ليس اختبار الوحدة بالضبط ، فهو أشبه باختبار التكامل لأنه يتضمن عدة فصول وليس فقط فئة واحدة)




هذا ما أستخدمه في الوقت الحاضر إذا تم إنتاج نتيجة الاختبار بشكل غير متزامن.

public class TestUtil {

    public static <R> R await(Consumer<CompletableFuture<R>> completer) {
        return await(20, TimeUnit.SECONDS, completer);
    }

    public static <R> R await(int time, TimeUnit unit, Consumer<CompletableFuture<R>> completer) {
        CompletableFuture<R> f = new CompletableFuture<>();
        completer.accept(f);
        try {
            return f.get(time, unit);
        } catch (InterruptedException | TimeoutException e) {
            throw new RuntimeException("Future timed out", e);
        } catch (ExecutionException e) {
            throw new RuntimeException("Future failed", e.getCause());
        }
    }
}

باستخدام الواردات الثابتة ، يقرأ الاختبار نوعًا ما لطيفًا. (ملاحظة ، في هذا المثال ، أبدأ مؤشر ترابط لتوضيح الفكرة)

    @Test
    public void testAsync() {
        String result = await(f -> {
            new Thread(() -> f.complete("My Result")).start();
        });
        assertEquals("My Result", result);
    }

إذا لم يتم استدعاء f.complete الاختبار بعد انتهاء المهلة. يمكنك أيضًا استخدام f.completeExceptionally للفشل مبكرًا.




من الجدير بالذكر أن هناك فصلاً مفيدًا للغاية Testing Concurrent Programs المتزامنة في التزامن العملي الذي يصف بعض مناهج اختبار الوحدة ويعطي حلولاً للمشكلات.




إذا كنت تستخدم CompletableFuture (الذي تم CompletableFuture في Java 8) أو SettableFuture (من Google Guava ) ، فيمكنك إنهاء الاختبار فور الانتهاء منه ، بدلاً من الانتظار لفترة محددة مسبقًا من الوقت. قد يبدو الاختبار كالتالي:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());



أجد مكتبة socket.io لاختبار منطق غير متزامن. يبدو طريقة بسيطة وجيزة باستخدام LinkedBlockingQueue . هذا example :

    @Test(timeout = TIMEOUT)
public void message() throws URISyntaxException, InterruptedException {
    final BlockingQueue<Object> values = new LinkedBlockingQueue<Object>();

    socket = client();
    socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
        @Override
        public void call(Object... objects) {
            socket.send("foo", "bar");
        }
    }).on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            values.offer(args);
        }
    });
    socket.connect();

    assertThat((Object[])values.take(), is(new Object[] {"hello client"}));
    assertThat((Object[])values.take(), is(new Object[] {"foo", "bar"}));
    socket.disconnect();
}

استخدام LinkedBlockingQueue تأخذ API لمنع حتى الحصول على نتيجة مثل طريقة متزامن. وتعيين مهلة لتجنب افتراض الكثير من الوقت لانتظار النتيجة.




تجنب الاختبار مع مؤشرات الترابط المتوازية كلما استطعت (وهو معظم الوقت). سيؤدي ذلك فقط إلى جعل اختباراتك غير مستقر (أحيانًا تمر ، وأحيانًا تفشل).

فقط عندما تحتاج إلى استدعاء بعض المكتبة / النظام ، قد تضطر إلى الانتظار في Awaitility أخرى ، وفي هذه الحالة ، استخدم دائمًا مكتبة Awaitility بدلاً من Thread.sleep() .

لا تدع الاتصال فقط get() أو join() في اختباراتك ، وإلا قد يتم تشغيل اختباراتك دائمًا على خادم CI الخاص بك في حالة عدم إكمال المستقبل أبدًا. أكّد دائمًا أن isDone() أولاً في اختباراتك قبل الاتصال get() . بالنسبة لـ CompletionStage ، أي .toCompletableFuture().isDone() .

عندما تختبر طريقة منع غير كالتالي:

public static CompletionStage<Foo> doSomething(BarService service) {
    CompletionStage<Bar> future = service.getBar();
    return future.thenApply(bar -> fooToBar());
}

ثم يجب عليك فقط اختبار النتيجة بتمرير "إكمال" في الاختبار ، يجب عليك أيضاً التأكد من أن الأسلوب الخاص بك doSomething() لا يمنع بواسطة استدعاء join() أو get() . هذا مهم على وجه الخصوص إذا كنت تستخدم إطار عمل غير محجوب.

للقيام بذلك ، قم بإجراء اختبار بمستقبل غير مكتمل قمت بتعيينه لإكماله يدويًا:

@Test
public void testDoSomething() {
    CompletableFuture<Bar> innerFuture = new CompletableFuture<>();
    fooResult = doSomething(() -> innerFuture).toCompletableFuture();
    assertFalse(fooResult.isDone());

    // this triggers the future to complete
    innerFuture.complete(new Bar());
    assertTrue(fooResult.isDone());

    // futher asserts about fooResult here
}

بهذه الطريقة ، إذا أضفت future.join() إلى doSomething () ، فسيفشل الاختبار.

إذا كانت الخدمة الخاصة بك تستخدم ExecutorService مثل thenApplyAsync(..., executorService) ، فحينئذٍ في الاختبارات الخاصة بك ، يتم إدخال ExecutorService واحد ، مثل واحد من الجوافة:

ExecutorService executorService = Executors.newSingleThreadExecutor();

إذا كانت التعليمات البرمجية الخاصة بك تستخدم forkJoinPool مثل thenApplyAsync(...) ، thenApplyAsync(...) كتابة التعليمات البرمجية لاستخدام ExecutorService (هناك العديد من الأسباب الجيدة) ، أو استخدم Awaitility.

لتقصير المثال ، جعلت BarService حجة أسلوب تم تنفيذها كأحد lamda Java8 في الاختبار ، وعادة ما يكون إشارة حقنها أنك سوف تسخر.




لا يوجد شيء خاطئ بطبيعته في اختبار الشفرة المترابطة / المترابطة ، خاصة إذا كان مؤشر الترابط هو نقطة الرمز الذي تختبره. الطريقة العامة لاختبار هذه الأشياء هي:

  • حظر مؤشر ترابط الاختبار الرئيسي
  • القبض على التأكيدات الفاشلة من مواضيع أخرى
  • إلغاء حظر مؤشر ترابط الاختبار الرئيسي
  • اردد اي اخفاقات

ولكن هذا كثير من القوالب الأساسية لاختبار واحد. نهج أفضل / أبسط هو مجرد استخدام ConcurrentUnit :

  final Waiter waiter = new Waiter();

  new Thread(() -> {
    doSomeWork();
    waiter.assertTrue(true);
    waiter.resume();
  }).start();

  // Wait for resume() to be called
  waiter.await(1000);

الفائدة من هذا النهج عبر CountdownLatch هو أنه أقل مطولاً لأن إخفاقات التأكيد التي تحدث في أي مؤشر ترابط يتم الإبلاغ عنها بشكل صحيح إلى الخيط الرئيسي ، بمعنى فشل الاختبار عندما يجب. هنا توجد عملية كتابة تقارن نهج CountdownLatch مع ConcurrentUnit.

كتبت أيضًا here حول الموضوع لأولئك الذين يريدون معرفة المزيد من التفاصيل.




طريقة واحدة وجدتها مفيدة جداً لاختبار أساليب غير متزامن يتم حقن مثيل Executor في مُنشئ الكائن إلى الاختبار. في الإنتاج ، تم تكوين مثيل المنفذ ليعمل بشكل غير متزامن بينما في الاختبار يمكن استهزائه للتشغيل بشكل متزامن.

لذا لنفترض أنني أحاول اختبار الطريقة غير المتزامنة Foo#doAsync(Callback c) ،

class Foo {
  private final Executor executor;
  public Foo(Executor executor) {
    this.executor = executor;
  }

  public void doAsync(Callback c) {
    executor.execute(new Runnable() {
      @Override public void run() {
        // Do stuff here
        c.onComplete(data);
      }
    });
  }
}

في الإنتاج ، سأقوم ببناء Foo مع نسخة Executors.newSingleThreadExecutor() Executor بينما في الاختبار من المحتمل أن أقوم ببنائه مع منفذ متزامن يقوم بما يلي -

class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable r) {
    r.run();
  }
}

الآن اختبار JUnit من الطريقة غير المتزامنة نظيفة جدا -

@Test public void testDoAsync() {
  Executor executor = new SynchronousExecutor();
  Foo objectToTest = new Foo(executor);

  Callback callback = mock(Callback.class);
  objectToTest.doAsync(callback);

  // Verify that Callback#onComplete was called using Mockito.
  verify(callback).onComplete(any(Data.class));

  // Assert that we got back the data that we expected.
  assertEquals(expectedData, callback.getData());
}



Related