راهی جدید برای تست برنامههای چندریسهای با استفاده از JUNIT
یکی از معروفترین ابزارهای تست برنامههای جاوا JUNIT است اما سختترین قسمت تست مربوط به آزمون برنامههای چندریسهای (multithread) است. در این مطلب سعی میکنیم با یک مثال نحوه تست این گونه برنامهها را نشان دهیم.
در برنامه زیر یک شمارنده داریم که قراراست به طور موازی با منطق کلی برنامه اجرا شود. پس یک کلاس Counter به شکل زیر داریم:
public class Counter { private int count=0; public void addOne() { count++; } public int getCount() { return count; } }
و یک کلاس TestCounter به شکل زیر تعریف میکنیم:
import static org.junit.Assert.assertEquals; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner; @RunWith(ConcurrentTestRunner.class) public class TestCounter { private Counter counter = new Counter(); @Test public void addOne() { counter.addOne(); } @After public void testCount() { assertEquals("4 Threads running addOne in parallel should lead to 4" , 4 , counter); } }
با استفاده از نماد RunWith در JUnit ، تستها با استفاده از ConcurrentTestRunner اجرا خواهند شد. این اجرا کننده تستها، متدی که با Test نمادگذاری شده باشد را در چهار ریسه موازی اجرا میکند. بعد از آن متدی که با After نمادگذاری شده باشد در ریسه اصلی (main) اجرا خواهد شد.
اگر که تست کیس را با یک race condition catcher مانند vmlens اجرا کنیم خواهیم دید:
یک race condition در دسترسی به فیلد count مشاهده میشود. برای حل این موضوع این فیلد را به صورت volatile تعریف میکنیم و تستها را اجرا میکنیم.
private volatile int count=0;
و الان است که تست کیس با موفقیت اجرا میشود. اگر چندبار این تست کیس را اجرا کنیم خواهیم دید که گاها با خطا روبرو میشود. برای این که ببینیم چه اتفاقی میافتد آن را با فعال کردن قابلیت “Delay synchronization for unit tests” در vmlens اجرا میکنیم آنگاه همواره خطای زیر را خواهیم دید:
java.lang.AssertionError: 4 Threads running addOne in parallel should lead to 4 expected:<4> but was:<3> at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645) at TestCounter.testCount(TestCounter.java:21) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at com.anarsoft.vmlens.concurrent.junit.internal.InvokeListOfMethods.evaluate(InvokeListOfMethods.java:23) at com.anarsoft.vmlens.concurrent.junit.internal.ConcurrentStatement.evaluateStatement(ConcurrentStatement.java:12) at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner.evaluateStatement(ConcurrentTestRunner.java:212) at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner.runChildrenConcurrently(ConcurrentTestRunner.java:172) at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner.access$0(ConcurrentTestRunner.java:78) at com.anarsoft.vmlens.concurrent.junit.ConcurrentTestRunner$1.evaluate(ConcurrentTestRunner.java:72) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
در حقیقت count++ یک عملیات نیست و ۶ عملیات در بایت کد را شامل میشود که یکی از آنها خواندن و دیگری نوشتن در فیلد count است:
ALOAD 0: this DUP GETFIELD Counter.count : int ICONST_1 IADD PUTFIELD Counter.count : int
با تعریف تاخیر بین این عملیات ها مطمئن خواهیم بود که دو ریسه این عملیاتها را موازی انجام میدهند. وقتی به طور موازی انجام میشوند پس count همواره از ۴ کمتر خواهد بود. گاهی سه و گاهی ۲ میباشد.
برای حل این مشکل لازم است متدها به صورت اتمیک تعریف شوند. برای اینکار از java.util.concurrent.atomic.AtomicInteger استفاده میکنیم:
import java.util.concurrent.atomic.AtomicInteger; public class Counter { private final AtomicInteger count= new AtomicInteger(); public void addOne() { count.incrementAndGet(); } public int getCount() { return count.get(); } }
به این ترتیب تست کیس همواره با موفقیت اجرا خواهد شد. (در این آموزش برای اجرای تستها از ابزار concurrent-junit و برای بررسی شرایط race از ابزار vmlens استفاده گردید.)
این آموزش روی یک مثال ساده انجام شد و میدانیم که در این نوع تستها فوت و فنهایی به خصوص زمانی که برنامهها و کلاسهای بزرگ و پیچیده را تست میکنیم وجود خواهد داشت پس درصورتی که سوالی بود در قسمت نظرات همین مطلب بفرمایید.
منبع:
https://dzone.com/articles/a-new-way-to-junit-test-your-multithreaded-java-co