قررت أن أشارك رؤيتي لاختبارات الوحدات ذات المعلمات ، وكيف نقوم بها ، وكيف لا تفعل ذلك على الأرجح (ولكنك تريد القيام بذلك).
أود أن أكتب عبارة جميلة حول ما يجب اختباره بشكل صحيح ، والاختبارات مهمة ، لكن الكثير من المواد قيلت بالفعل وكُتبت قبلي ، سأحاول فقط تلخيص وإبراز ما ، في رأيي ، نادرًا ما يستخدمه الناس (يفهمون) ، والذي بشكل أساسي التحركات في.
الهدف الرئيسي للمقالة هو إظهار كيف يمكنك (ويجب) التوقف عن تشويش اختبار الوحدة الخاص بك برمز لإنشاء الكائنات ، وكيفية إنشاء بيانات الاختبار بشكل إعلاني إذا لم تكن محاكاة (أي ()) كافية ، وهناك الكثير من هذه المواقف.
دعونا ننشئ مشروعًا مخضرمًا ، نضيف junit5 ، junit-jupiter-params و mokito إليه
حتى لا يكون مملًا تمامًا ، سنبدأ الكتابة على الفور من الاختبار ، كما يحب المدافعون عن TDD ، نحتاج إلى خدمة سنختبرها بشكل إعلاني ، أي شيء سيفعله ، فليكن خدمة HabrService.
لنقم بإنشاء اختبار HabrServiceTest. أضف رابطًا إلى HabrService في حقل فئة الاختبار:
public class HabrServiceTest {
private HabrService habrService;
@Test
void handleTest(){
}
}
أنشئ خدمة عبر بيئة تطوير متكاملة (بالضغط برفق على الاختصار) ، أضف التعليق التوضيحيInjectMocks إلى الحقل.
لنبدأ مباشرة بالاختبار: سيكون لدى HabrService في تطبيقنا الصغير طريقة مقبض واحد () تأخذ وسيطة HabrItem واحدة ، والآن يبدو اختبارنا كما يلي:
public class HabrServiceTest {
@InjectMocks
private HabrService habrService;
@Test
void handleTest(){
HabrItem item = new HabrItem();
habrService.handle(item);
}
}
دعنا نضيف التابع handle () إلى HabrService ، والذي سيعيد معرف المنشور الجديد على Habré بعد أن يتم الإشراف عليه وحفظه في قاعدة البيانات ، ويأخذ نوع HabrItem ، وسننشئ أيضًا HabrItem ، والآن يجمع الاختبار ، لكن يتعطل.
النقطة المهمة هي أننا أضفنا فحصًا لقيمة الإرجاع المتوقعة.
public class HabrServiceTest {
@InjectMocks
private HabrService habrService;
@BeforeEach
void setUp(){
initMocks(this);
}
@Test
void handleTest() {
HabrItem item = new HabrItem();
Long actual = habrService.handle(item);
assertEquals(1L, actual);
}
}
أيضًا ، أريد أن أتأكد من أنه أثناء استدعاء طريقة المقبض () ، تم استدعاء ReviewService و PersistanceService ، وتم استدعاؤهما بدقة واحدة تلو الأخرى ، وعملوا مرة واحدة بالضبط ، ولم يعد يتم استدعاء أي طرق أخرى. بمعنى آخر ، مثل هذا:
public class HabrServiceTest {
@InjectMocks
private HabrService habrService;
@BeforeEach
void setUp(){
initMocks(this);
}
@Test
void handleTest() {
HabrItem item = new HabrItem();
Long actual = habrService.handle(item);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(item);
inOrder.verify(persistenceService).makePersist(item);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
}
أضف reviewService و persistentService إلى حقلي الفصل ، وأنشئهما ، وأضف أساليب makeRewiew () و makePersist () إليهما على التوالي. الآن يتم تجميع كل شيء ، لكن الاختبار أحمر بالطبع.
في سياق هذه المقالة ، لا تعد تطبيقات ReviewService و PersistanceService مهمة جدًا ، كما أن تطبيق HabrService مهم ، فلنجعله أكثر تشويقًا مما هو عليه الآن:
public class HabrService {
private final ReviewService reviewService;
private final PersistenceService persistenceService;
public HabrService(final ReviewService reviewService, final PersistenceService persistenceService) {
this.reviewService = reviewService;
this.persistenceService = persistenceService;
}
public Long handle(final HabrItem item) {
HabrItem reviewedItem = reviewService.makeRewiew(item);
Long persistedItemId = persistenceService.makePersist(reviewedItem);
return persistedItemId;
}
}
وباستخدام عندما (). ثم () نقوم بتثبيت سلوك المكونات المساعدة ، ونتيجة لذلك ، أصبح اختبارنا على هذا النحو والآن أصبح أخضر:
public class HabrServiceTest {
@Mock
private ReviewService reviewService;
@Mock
private PersistenceService persistenceService;
@InjectMocks
private HabrService habrService;
@BeforeEach
void setUp() {
initMocks(this);
}
@Test
void handleTest() {
HabrItem source = new HabrItem();
HabrItem reviewedItem = mock(HabrItem.class);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
}
نموذج بالحجم الطبيعي لإثبات قوة الاختبارات المحددة جاهز.
أضف حقلاً بنوع المحور ، hubType إلى نموذج الطلب الخاص بنا لخدمة HabrItem ، قم بإنشاء Enum HubType وقم بتضمين عدة أنواع فيه:
public enum HubType {
JAVA, C, PYTHON
}
وبالنسبة لنموذج HabrItem ، قم بإضافة دالة getter و setter إلى حقل HubType الذي تم إنشاؤه.
لنفترض أن مفتاحًا مخفيًا في أعماق خدمة HabrService الخاصة بنا ، والتي ، اعتمادًا على نوع المحور ، تقوم بشيء غير معروف مع الطلب ، وفي الاختبار نريد اختبار كل حالة من حالات المجهول ، سيبدو التطبيق الساذج للطريقة كما يلي:
@Test
void handleTest() {
HabrItem reviewedItem = mock(HabrItem.class);
HabrItem source = new HabrItem();
source.setHubType(HubType.JAVA);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
يمكنك جعلها أجمل قليلاً وأكثر ملاءمة عن طريق جعل معلمة الاختبار وإضافة قيمة عشوائية من التعداد الخاص بنا كمعامل ، ونتيجة لذلك ، سيبدو إعلان الاختبار كما يلي:
@ParameterizedTest
@EnumSource(HubType.class)
void handleTest(final HubType type)
بشكل جيد ، بشكل إعلاني ، وسيتم استخدام جميع قيم تعدادنا بالتأكيد في بعض الاختبارات التالية ، يحتوي التعليق التوضيحي على معلمات ، ويمكننا إضافة استراتيجيات للتضمين والاستبعاد.
لكن ربما لم أقنعك بأن الاختبارات ذات المعلمات جيدة. اضف إليه
طلب HabrItem الأصلي هو حقل تحرير جديد ، حيث سيتم كتابة عدد آلاف المرات التي يعدل فيها مستخدمو Habr مقالتهم قبل النشر ، بحيث تعجبك قليلاً على الأقل ، وافترض أنه في مكان ما في أعماق خدمة HabrService يوجد نوع من المنطق يفعل المجهول شيئًا ما ، اعتمادًا على مقدار ما حاول المؤلف ، ماذا لو كنت لا أرغب في كتابة 5 أو 55 اختبارًا لجميع خيارات editCount الممكنة ، لكنني أريد الاختبار بشكل إعلاني ، وفي مكان ما في مكان واحد ، أشر على الفور إلى جميع القيم التي أود التحقق منها ... لا يوجد شيء أبسط ، وباستخدام API للاختبارات ذات المعلمات ، نحصل على شيء مثل هذا في إعلان الطريقة:
@ParameterizedTest
@ValueSource(ints = {0, 5, 14, 23})
void handleTest(final int type)
هناك مشكلة ، نريد جمع قيمتين في معلمات طريقة الاختبار في وقت واحد بشكل إعلاني ، يمكنك استخدام طريقة ممتازة أخرى للاختبارات ذات المعلماتCsvSource ، وهي مثالية لاختبار المعلمات البسيطة ، مع قيمة إخراج بسيطة (مريحة للغاية لاختبار فئات المرافق) ، ولكن ماذا إذا أصبح الكائن أكثر تعقيدًا؟ لنفترض أنه سيحتوي على حوالي 10 حقول ، وليس فقط الأنواع الأولية وجافا.
يأتي التعليق التوضيحيMethodSource للإنقاذ ، وأصبحت طريقة الاختبار لدينا أقصر بشكل ملحوظ ولم يعد هناك محددون فيها ، ويتم تغذية مصدر الطلب الوارد إلى طريقة الاختبار كمعامل:
@ParameterizedTest
@MethodSource("generateSource")
void handleTest(final HabrItem source) {
HabrItem reviewedItem = mock(HabrItem.class);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
يحتوي التعليق التوضيحيMethodSource على سلسلة إنشاء مصدر ، فما هو؟ هذا هو اسم الطريقة التي ستجمع النموذج المطلوب لنا ، سيبدو إعلانها كما يلي:
private static Stream<Arguments> generateSource() {
HabrItem habrItem = new HabrItem();
habrItem.setHubType(HubType.JAVA);
habrItem.setEditCount(999L);
return nextStream(() -> habrItem);
}
للراحة ، قمت بنقل تشكيل دفق من وسيطات nextStream إلى فئة اختبار فائدة منفصلة:
public class CommonTestUtil {
private static final Random RANDOM = new Random();
public static <T> Stream<Arguments> nextStream(final Supplier<T> supplier) {
return Stream.generate(() -> Arguments.of(supplier.get())).limit(nextIntBetween(1, 10));
}
public static int nextIntBetween(final int min, final int max) {
return RANDOM.nextInt(max - min + 1) + min;
}
}
الآن ، عند بدء الاختبار ، سيتم إضافة نموذج طلب HabrItem بشكل إعلاني إلى معلمة طريقة الاختبار ، وسيتم تشغيل الاختبار عدة مرات مثل عدد الحجج التي تم إنشاؤها بواسطة أداة الاختبار الخاصة بنا ، في حالتنا من 1 إلى 10.
يمكن أن يكون هذا مناسبًا بشكل خاص إذا كان النموذج في تدفق الحجج لا يتم جمعها عن طريق الكود الصلب ، كما في مثالنا ، ولكن بمساعدة الموزعين العشوائيين. (اختبارات العائمة طويلة الأمد ، ولكن إذا كانت كذلك ، فهناك مشكلة أيضًا).
في رأيي ، كل شيء رائع بالفعل ، والاختبار الآن يصف فقط سلوك بذرة ، والنتائج المتوقعة.
لكن ها هو الحظ السيئ ، حقل جديد ، نص ، مصفوفة من السلاسل تضاف إلى نموذج HabrItem ، والتي قد تكون أو لا تكون كبيرة جدًا ، لا يهم ، الشيء الرئيسي هو أننا لا نريد تشويش اختباراتنا ، لا نحتاج إلى بيانات عشوائية ، نريد نموذجًا محددًا بدقة ، ببيانات محددة ، وجمعها في اختبار أو في أي مكان آخر - لا نريد ذلك. سيكون رائعًا إذا كان بإمكانك أخذ نص طلب json من أي مكان ، على سبيل المثال من ساعي البريد ، وإنشاء ملف وهمي بناءً عليه ، وتشكيل نموذج بشكل إعلاني في الاختبار ، مع تحديد المسار إلى ملف json بالبيانات فقط.
ممتاز. نستخدم التعليق التوضيحيJsonSource ، والذي سيأخذ معلمة مسار ، مع مسار ملف نسبي وفئة مستهدفة. تبا! لا يوجد مثل هذا التعليق التوضيحي في الاختبارات ذات المعلمات ، لكني أود ذلك.
دعونا نكتبها بأنفسنا.
ArgumentsProvider هي المسؤولة عن معالجة جميع التعليقات التوضيحية المضمنة فيParametrizedTest في junit ، وسوف نكتب JsonArgumentProvider الخاص بنا:
public class JsonArgumentProvider implements ArgumentsProvider, AnnotationConsumer<JsonSource> {
private String path;
private MockDataProvider dataProvider;
private Class<?> clazz;
@Override
public void accept(final JsonSource jsonSource) {
this.path = jsonSource.path();
this.dataProvider = new MockDataProvider(new ObjectMapper());
this.clazz = jsonSource.clazz();
}
@Override
public Stream<Arguments> provideArguments(final ExtensionContext context) {
return nextSingleStream(() -> dataProvider.parseDataObject(path, clazz));
}
}
MockDataProvider هي فئة لتحليل ملفات json الوهمية ، وتنفيذها بسيط للغاية:
public class MockDataProvider {
private static final String PATH_PREFIX = "json/";
private final ObjectMapper objectMapper;
public <T> T parseDataObject(final String name, final Class<T> clazz) {
return objectMapper.readValue(new ClassPathResource(PATH_PREFIX + name).getInputStream(), clazz);
}
}
الموفر الوهمي جاهز ، وموفر الوسيطة للتعليق التوضيحي أيضًا ، ويبقى إضافة التعليق التوضيحي نفسه:
/**
* Source- ,
* json-
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(JsonArgumentProvider.class)
public @interface JsonSource {
/**
* json-, classpath:/json/
*
* @return
*/
String path() default "";
/**
* ,
*
* @return
*/
Class<?> clazz();
}
الصيحة. شرحنا جاهز للاستخدام ، طريقة الاختبار الآن:
@ParameterizedTest
@JsonSource(path = MOCK_FILE_PATH, clazz = HabrItem.class)
void handleTest(final HabrItem source) {
HabrItem reviewedItem = mock(HabrItem.class);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
في mock json ، يمكننا إنتاج الكثير وبسرعة كبيرة مجموعة من العناصر التي نحتاجها ، وليس هناك من أي مكان من الآن فصاعدًا رمز يصرف الانتباه عن جوهر الاختبار ، لتكوين بيانات الاختبار ، بالطبع ، يمكنك غالبًا القيام به باستخدام mocks ، ولكن ليس دائمًا.
بإيجاز ، أود أن أقول ما يلي: غالبًا ما نعمل كما اعتدنا على العمل ، لسنوات ، دون التفكير في أن بعض الأشياء يمكن القيام بها بشكل جميل وبسيط ، وغالبًا ما نستخدم واجهة برمجة التطبيقات القياسية للمكتبات التي استخدمناها منذ سنوات ، لكننا لا نعرف كل قدراتها.
ملاحظة: المقالة ليست محاولة لمعرفة مفاهيم TDD ، فقد أردت إضافة بيانات الاختبار إلى حملة سرد القصص لجعلها أكثر وضوحًا وإثارة للاهتمام.