Thử nghiệm JUnit với đầu vào của người dùng được mô phỏng


81

Tôi đang cố gắng tạo một số thử nghiệm JUnit cho một phương pháp yêu cầu người dùng nhập. Phương pháp đang thử nghiệm trông giống như phương pháp sau:

public static int testUserInput() {
    Scanner keyboard = new Scanner(System.in);
    System.out.println("Give a number between 1 and 10");
    int input = keyboard.nextInt();

    while (input < 1 || input > 10) {
        System.out.println("Wrong number, try again.");
        input = keyboard.nextInt();
    }

    return input;
}

Có cách nào có thể để tự động chuyển chương trình một int thay vì tôi hoặc người khác thực hiện việc này theo cách thủ công trong phương pháp kiểm tra JUnit không? Giống như mô phỏng đầu vào của người dùng?

Cảm ơn trước.


7
Chính xác thì bạn đang thử nghiệm cái gì? Máy quét? Một phương pháp thử nghiệm thường phải khẳng định điều gì đó là hữu ích.
Markus

2
Bạn không cần phải kiểm tra lớp Máy quét của Java. Bạn có thể đặt đầu vào theo cách thủ công và chỉ cần kiểm tra logic của riêng bạn. int input = -1 hoặc 5 hoặc 11 sẽ bao gồm logic của bạn
blong824

3
Sáu năm sau ... và nó vẫn là một câu hỏi hay. Không kém phần quan trọng vì khi bắt đầu phát triển một ứng dụng, bạn có thể không muốn có tất cả các chuông và còi của JavaFX, mà thay vào đó chỉ cần bắt đầu sử dụng dòng lệnh khiêm tốn để có một chút tương tác rất cơ bản. Thật xấu hổ khi JUnit không làm cho việc này dễ dàng hơn một chút. Đối với tôi câu trả lời Omar Elshal là rất tốt đẹp, với tối thiểu "contrived" hoặc "bóp méo" ứng dụng mã hóa có liên quan ...
mike động vật gặm nhấm

Câu trả lời:


103

Bạn có thể thay thế System.in bằng luồng của riêng bạn bằng cách gọi System.setIn (InputStream in) . InputStream có thể là một mảng byte:

InputStream sysInBackup = System.in; // backup System.in to restore it later
ByteArrayInputStream in = new ByteArrayInputStream("My string".getBytes());
System.setIn(in);

// do your thing

// optionally, reset System.in to its original
System.setIn(sysInBackup);

Có thể làm cho phương pháp này dễ kiểm tra hơn bằng cách chuyển IN và OUT dưới dạng các tham số:

public static int testUserInput(InputStream in,PrintStream out) {
    Scanner keyboard = new Scanner(in);
    out.println("Give a number between 1 and 10");
    int input = keyboard.nextInt();

    while (input < 1 || input > 10) {
        out.println("Wrong number, try again.");
        input = keyboard.nextInt();
    }

    return input;
}

5
@KrzyH làm cách nào để thực hiện việc này với nhiều đầu vào? Nói rằng trong // làm việc của bạn, tôi có ba lời nhắc cho người dùng nhập. Làm thế nào tôi sẽ đi về điều đó?
Stupid.Fat.Cat

1
@ Stupid.Fat.Cat Tôi đã thêm cách tiếp cận thứ hai cho vấn đề, thanh lịch hơn và đáng tin cậy hơn
KrzyH

1
@KrzyH Đối với cách tiếp cận thứ hai, bạn sẽ được yêu cầu chuyển luồng đầu vào và luồng đầu ra dưới dạng tham số trong định nghĩa phương thức, đây là điều tôi không tìm kiếm. Bạn có biết cách nào tốt hơn không?
Chirag

6
Làm cách nào để mô phỏng việc nhấn phím enter? Ngay bây giờ, chương trình chỉ đọc tất cả đầu vào trong một lần, điều này không tốt. Tôi đã cố gắng \nnhưng điều đó không tạo sự khác biệt
CodyBugstein

3
@CodyBugstein Sử dụng ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());để nhận nhiều đầu vào cho các keyboard.nextLine()cuộc gọi khác nhau .
A Jar of Clay

18

Để kiểm tra mã của bạn, bạn nên tạo một trình bao bọc cho các chức năng nhập / xuất hệ thống. Bạn có thể thực hiện việc này bằng cách sử dụng phương pháp tiêm phụ thuộc, cung cấp cho chúng tôi một lớp có thể yêu cầu các số nguyên mới:

public static class IntegerAsker {
    private final Scanner scanner;
    private final PrintStream out;

    public IntegerAsker(InputStream in, PrintStream out) {
        scanner = new Scanner(in);
        this.out = out;
    }

    public int ask(String message) {
        out.println(message);
        return scanner.nextInt();
    }
}

Sau đó, bạn có thể tạo các bài kiểm tra cho chức năng của mình, bằng cách sử dụng một khuôn khổ giả (tôi sử dụng Mockito):

@Test
public void getsIntegerWhenWithinBoundsOfOneToTen() throws Exception {
    IntegerAsker asker = mock(IntegerAsker.class);
    when(asker.ask(anyString())).thenReturn(3);

    assertEquals(getBoundIntegerFromUser(asker), 3);
}

@Test
public void asksForNewIntegerWhenOutsideBoundsOfOneToTen() throws Exception {
    IntegerAsker asker = mock(IntegerAsker.class);
    when(asker.ask("Give a number between 1 and 10")).thenReturn(99);
    when(asker.ask("Wrong number, try again.")).thenReturn(3);

    getBoundIntegerFromUser(asker);

    verify(asker).ask("Wrong number, try again.");
}

Sau đó, viết hàm của bạn vượt qua các bài kiểm tra. Hàm này gọn gàng hơn nhiều vì bạn có thể loại bỏ sự trùng lặp yêu cầu / nhận số nguyên và các lệnh gọi hệ thống thực tế được đóng gói.

public static void main(String[] args) {
    getBoundIntegerFromUser(new IntegerAsker(System.in, System.out));
}

public static int getBoundIntegerFromUser(IntegerAsker asker) {
    int input = asker.ask("Give a number between 1 and 10");
    while (input < 1 || input > 10)
        input = asker.ask("Wrong number, try again.");
    return input;
}

Điều này có vẻ như quá mức cần thiết đối với ví dụ nhỏ của bạn, nhưng nếu bạn đang xây dựng một ứng dụng lớn hơn đang phát triển như thế này thì có thể thu được lợi nhuận khá nhanh.


5

Một cách phổ biến để kiểm tra mã tương tự là trích xuất một phương pháp có trong Máy quét và Máy in, tương tự như câu trả lời StackOverflow này và kiểm tra rằng:

public void processUserInput() {
  processUserInput(new Scanner(System.in), System.out);
}

/** For testing. Package-private if possible. */
public void processUserInput(Scanner scanner, PrintWriter output) {
  output.println("Give a number between 1 and 10");
  int input = scanner.nextInt();

  while (input < 1 || input > 10) {
    output.println("Wrong number, try again.");
    input = scanner.nextInt();
  }

  return input;
}

Xin lưu ý rằng bạn sẽ không thể đọc đầu ra của mình cho đến khi kết thúc và bạn sẽ phải chỉ định trước tất cả đầu vào của mình:

@Test
public void shouldProcessUserInput() {
  StringWriter output = new StringWriter();
  String input = "11\n"       // "Wrong number, try again."
               + "10\n";

  assertEquals(10, systemUnderTest.processUserInput(
      new Scanner(input), new PrintWriter(output)));

  assertThat(output.toString(), contains("Wrong number, try again.")););
}

Tất nhiên, thay vì tạo một phương thức quá tải, bạn cũng có thể giữ "máy quét" và "đầu ra" dưới dạng các trường có thể thay đổi trong hệ thống của bạn đang được thử nghiệm. Tôi có xu hướng muốn giữ các lớp học càng không có trạng thái càng tốt, nhưng đó không phải là một nhượng bộ quá lớn nếu nó quan trọng với bạn hoặc đồng nghiệp / người hướng dẫn của bạn.

Bạn cũng có thể chọn đặt mã thử nghiệm của mình trong cùng một gói Java với mã đang thử nghiệm (ngay cả khi nó nằm trong một thư mục nguồn khác), điều này cho phép bạn giảm khả năng hiển thị của quá tải hai tham số thành gói riêng tư.


Ý của bạn là để kiểm tra, phương thức processUserInput () gọi phương thức (vào, ra) chứ không phải processUserInput (vào, ra)?
NadZ 21/12/16

@NadZ Tất nhiên; Tôi bắt đầu với một ví dụ chung và thay đổi hoàn toàn nó trở lại cụ thể cho câu hỏi.
Jeff Bowman

3

Tôi đã tìm ra một cách đơn giản hơn. Tuy nhiên, bạn phải sử dụng thư viện bên ngoài System.rules của @Stefan Birkner

Tôi chỉ lấy ví dụ được cung cấp ở đó, tôi nghĩ nó không thể trở nên đơn giản hơn:

import java.util.Scanner;

public class Summarize {
  public static int sumOfNumbersFromSystemIn() {
    Scanner scanner = new Scanner(System.in);
    int firstSummand = scanner.nextInt();
    int secondSummand = scanner.nextInt();
    return firstSummand + secondSummand;
  }
}

Kiểm tra

import static org.junit.Assert.*;
import static org.junit.contrib.java.lang.system.TextFromStandardInputStream.*;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.TextFromStandardInputStream;

public class SummarizeTest {
  @Rule
  public final TextFromStandardInputStream systemInMock
    = emptyStandardInputStream();

  @Test
  public void summarizesTwoNumbers() {
    systemInMock.provideLines("1", "2");
    assertEquals(3, Summarize.sumOfNumbersFromSystemIn());
  }
}

Tuy nhiên, vấn đề trong trường hợp đầu vào thứ hai của tôi có khoảng trắng và điều này làm cho toàn bộ luồng đầu vào rỗng!


Điều này hoạt động rất tốt ... Tôi không biết ý bạn là "input stream null". Điều tốt là inputEntry = scanner.nextLine();khi sử dụng với System.insẽ luôn đợi người dùng (và sẽ chấp nhận một dòng trống là dòng trống String) ... trong khi khi bạn cung cấp các dòng bằng cách sử dụng systemInMock.provideLines()này sẽ ném NoSuchElementExceptionkhi hết dòng. Điều này thực sự giúp bạn không dễ dàng "bóp méo" mã ứng dụng quá nhiều để phục vụ cho quá trình thử nghiệm.
loài gặm nhấm mike

Tôi không nhớ chính xác vấn đề là gì, nhưng bạn nói đúng, tôi đã kiểm tra lại mã của mình và nhận thấy rằng tôi đã sửa nó theo hai cách: 1. Sử dụng systemInMock: systemInMock.provideLines("1", "2"); 2. Sử dụng System.setIn mà không có thư viện bên ngoài:String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));
Omar Elshal

2

Bạn có thể bắt đầu bằng cách trích xuất logic truy xuất số từ bàn phím thành phương pháp riêng của nó. Sau đó, bạn có thể kiểm tra logic xác thực mà không cần lo lắng về bàn phím. Để kiểm tra lệnh gọi keyboard.nextInt (), bạn có thể muốn xem xét sử dụng một đối tượng giả.


2
Lưu ý, bạn không thể giả lập các đối tượng Máy quét (chúng là cuối cùng).
hoipolloi

2

Tôi đã khắc phục sự cố về việc đọc từ stdin để mô phỏng bảng điều khiển ...

Vấn đề của tôi là tôi muốn thử viết trong JUnit, kiểm tra bảng điều khiển để tạo một đối tượng nhất định ...

Vấn đề giống như tất cả những gì bạn nói: Làm thế nào tôi có thể viết trong bài kiểm tra Stdin từ JUnit?

Sau đó, ở trường đại học, tôi học về chuyển hướng giống như bạn nói System.setIn (InputStream) thay đổi bộ ký hiệu stdin và sau đó bạn có thể viết ...

Nhưng có một vấn đề nữa cần khắc phục ... khối kiểm tra JUnit đang đợi đọc từ InputStream mới của bạn, vì vậy bạn cần tạo một chuỗi để đọc từ InputStream và từ kiểm tra JUnit. Viết chuỗi trong Stdin mới ... Đầu tiên bạn phải viết trong Stdin vì nếu bạn viết sau khi tạo Chủ đề để đọc từ stdin, bạn có thể sẽ có Điều kiện chủng tộc ... bạn có thể viết trong InputStream trước để đọc hoặc bạn có thể đọc từ InputStream trước khi ghi ...

Đây là mã của tôi, kỹ năng tiếng Anh của tôi kém, tôi hy vọng tất cả các bạn có thể hiểu được vấn đề và giải pháp để mô phỏng viết bằng stdin từ bài kiểm tra JUnit.

private void readFromConsole(String data) throws InterruptedException {
    System.setIn(new ByteArrayInputStream(data.getBytes()));

    Thread rC = new Thread() {
        @Override
        public void run() {
            study = new Study();
            study.read(System.in);
        }
    };
    rC.start();
    rC.join();      
}

1

Tôi thấy hữu ích khi tạo một giao diện xác định các phương thức tương tự như java.io.Console và sau đó sử dụng nó để đọc hoặc ghi vào System.out. Việc triển khai thực sự sẽ ủy quyền cho System.console () trong khi phiên bản JUnit của bạn có thể là một đối tượng giả với đầu vào soạn sẵn và các phản hồi mong đợi.

Ví dụ: bạn sẽ xây dựng MockConsole chứa dữ liệu đầu vào được soạn sẵn từ người dùng. Việc triển khai mô hình sẽ bật một chuỗi đầu vào ra khỏi danh sách mỗi khi readLine được gọi. Nó cũng sẽ tập hợp tất cả đầu ra được ghi vào danh sách các phản hồi. Vào cuối bài kiểm tra, nếu mọi thứ diễn ra tốt đẹp, thì tất cả đầu vào của bạn sẽ được đọc và bạn có thể khẳng định trên đầu ra.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.