如何测试这个文件写入,3lines功能?

时间:2017-01-23 14:44:32

标签: java unit-testing testing mockito writer

这是我在某些服务类中的方法。它是公开的,所以应该进行测试。我根本不知道应该测试什么。我嘲笑public class MainActivity extends Activity { private interface GooglePlacesClient { @GET("/maps/api/place/autocomplete/json") Observable<PlacesResult> autocomplete( @Query("key") String key, @Query("input") String input); } private class PlacesResult { @Expose List<MainActivity.Prediction> predictions; @Expose String status; } private class Prediction { @Expose String description; } private static final String LOG_TAG = "RxRetrofitAutoComplete"; private static final String GOOGLE_API_BASE_URL = "https://maps.googleapis.com"; private static final String API_KEY = "XXX"; private static final int DELAY = 500; GooglePlacesClient mGooglePlacesClient; @InjectView(R.id.editText1) EditText editText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.inject(this); if (API_KEY.length()<10) { Toast.makeText(this, "API KEY is unset!", Toast.LENGTH_LONG).show(); return; } if (mGooglePlacesClient == null) { mGooglePlacesClient = new RestAdapter.Builder() .setConverter(new GsonConverter(new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create())) .setEndpoint(GOOGLE_API_BASE_URL) .setLogLevel(RestAdapter.LogLevel.FULL).build() .create(GooglePlacesClient.class); } Observable<EditText> searchTextObservable = ViewObservable.text(editText); searchTextObservable.debounce(DELAY, TimeUnit.MILLISECONDS) .map(new Func1<EditText, String>() { @Override public String call(EditText editText) { return editText.getText().toString(); } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<String>() { @Override public void call(String s) { Log.d(LOG_TAG, s); try { mGooglePlacesClient .autocomplete(API_KEY, URLEncoder.encode(s, "utf8")) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<PlacesResult>() { @Override public void call(PlacesResult placesResult) { List<String> strings = new ArrayList<String>(); for (MainActivity.Prediction p : placesResult.predictions) { strings.add(p.description); } ListView listView = (ListView) findViewById(R.id.listView1); if (listView != null) { listView.setAdapter(new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1, strings)); } } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { throwable.printStackTrace(); } }); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { throwable.printStackTrace(); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } @Override protected void onStop() { super.onStop(); } } 和spyOn函数调用,但是这个实现它是不可能的(不是吗?)

我正在使用WriterMockito

现在,我只能使函数抛出并断言该异常

任何帮助?

JUnit

4 个答案:

答案 0 :(得分:4)

如果您觉得添加特殊内容是业务逻辑,因此是您的类的职责,那么创建FileWriter不是(根据单一责任模式

因此,您应该使用已在测试下的类中调用的FileWriterFactory。然后你可以模拟FileWriterFactory来重新调用Writer接口的模拟实现,然后你可以检查它是否有预期的字符串。

你的CuT会改为:

private final WriterFactory writerFactory;

public ClassUnderTest(@Inject WriterFactory writerFactory){
   this.writerFactory = writerFactory;
}

@Override
public void initIndexFile(File emptyIndexFile) {
    try {
        Writer writer = writerFactory.create(emptyIndexFile);
        writer.write("[]");
        writer.close();
    } catch (IOException e) {
        throw new IndexFileInitializationException(
            "Error initialization index file " + emptyIndexFile.getPath()
        );
    }
}

和你的测试:

class Test{

  @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); 

  @Mock
  private FileWriterFactory fileWriterFactory;
  private Writer fileWriter = spy(new StringWriter());
  File anyValidFile = new File(".");
  @Test
  public void initIndexFile_validFile_addsEmptyraces(){
     //arrange
     doReturn(fileWriter).whenn(fileWriterFactory).create(any(File.class));

     // act
     new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);

     //assert
     verify(fileWriterFactory)create(anyValidFile);
     assertEquals("text written to File", "[]", fileWriter.toString());
     verify(fileWriter).close();
  }
}

此外,您可以轻松检查切割是否截获IOException:

  @Rule
  public ExpectedException exception = ExpectedException.none();

  @Test
  public void initIndexFile_missingFile_IndexFileInitializationException(){
     //arrange
     doReturnThrow(new IOException("UnitTest")).whenn(fileWriterFactory).create(any(File.class));

     //assert
     exception.expect(IndexFileInitializationException.class);
     exception.expectMessage("Error initialization index file "+anyValidFile.getPath());

     // act
     new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
  }
  

尼斯!一家工厂只是为了测试3行代码! - 尼古拉斯·菲洛托

这是一个好点。

问题是:该类是否有任何方法直接与File对象进行交互,需要在之后创建FileWriter?

如果答案是“否”(因为它很可能)遵循KISS原则,您应该直接注入Writer对象而不是工厂,并使用没有File参数的方法。

private final Writer writer;

public ClassUnderTest(@Inject Writer writer){
   this.writer = writer;
}

@Override
public void initIndexFile() {
    try {
        writer.write("[]");
        writer.close();
    } catch (IOException e) {
        throw new IndexFileInitializationException(
            "Error initialization index file " + emptyIndexFile.getPath()
        );
    }
}

修改测试:

class Test{       
  @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); 
  @Rule public ExpectedException exception = ExpectedException.none();

  @Mock
  private FileWriterFactory fileWriterFactory;
  @Mock
  private Writer failingFileWriter;
  private Writer validFileWriter = spy(new StringWriter());
  File anyValidFile = new File(".");
  @Test
  public void initIndexFile_validFile_addsEmptyraces(){
     //arrange         
     // act
     new ClassUnderTest(validFileWriter).initIndexFile();

     //assert
     verify(fileWriterFactory)create(anyValidFile);
     assertEquals("text written to File", "[]", fileWriter.toString());
     verify(fileWriter).close();
  }

  @Test
  public void initIndexFile_missingFile_IndexFileInitializationException(){
     //arrange
     doReturnThrow(new IOException("UnitTest")).whenn(failingFileWriter).whire(anyString());

     //assert
     exception.expect(IndexFileInitializationException.class);
     exception.expectMessage("Error initialization index file "+anyValidFile.getPath());

     // act
     new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
  }
}

答案 1 :(得分:0)

要测试您的方法是否可以正确地与作者交互,通过发送正确的命令,您的Pogram必须暴露某种&#34; seam&#34;以便您的测试可以配置模拟FileWriter。我不熟悉mockito,但有一种方法是将FileWriter实例封装在方法后面,然后您的测试可以覆盖该方法以返回模拟FileWriter

假设File是一个接口:

public Writer getFileWriter(File emptyIndexFile) {
   return new FileWriter(emptyIndexFile);
}

这可以允许您覆盖测试的上述方法并返回假的Writer

@Override
public Writer getFileWriter(File emptyIndexFile) {
   return mockFileWriterInstance;
}

然后你的测试可以进行练习initIndexFile并对操作进行断言。使用模拟文件编写器可以轻而易举地抛出IOException,以便您可以使用错误处理逻辑。

答案 2 :(得分:0)

您只需在测试中为方法提供一个临时文件,只需按预期检查它是否包含[],然后再删除该文件。

类似的东西:

public class FileWritingTest {

    // File to provide to the method initIndexFile
    private File file;

    /* This is executed before the test */
    @Before
    public void init() throws IOException {
        // Create a temporary file
        this.file = File.createTempFile("FileWritingTest", "tmp");
        // Indicates that it should be removed on exit
        file.deleteOnExit();
    }

    /* This is executed after the test */
    @After
    public void clean() throws IOException {
        // Delete the file once test over
        file.delete();
    }

    @Test
    public void testInitIndexFile() throws IOException {
        FileWriting fw = new FileWriting();
        // Call the method
        fw.initIndexFile(this.file);
        // Check that the content is [] as expected
        Assert.assertEquals("[]", new String(Files.readAllBytes(file.toPath()))); 
    }
}

NB 1:我依赖于new String(byte[]),这意味着我依赖于您在当前代码中执行的默认字符编码,但这不是一个好习惯,我们应该设置一个显式字符编码以避免平台相关。

注意2:假设您使用的是java 7或更高版本,您应该考虑使用 try-with-resources 语句来正确关闭您的编写器,然后您的代码就会是:

public void initIndexFile(File emptyIndexFile) {
    try (Writer writer = new FileWriter(emptyIndexFile)) {
        writer.write("[]");
    } catch (IOException e) {
        throw new IndexFileInitializationException(
            "Error initialization index file " + emptyIndexFile.getPath()
        );
    }
}

答案 3 :(得分:0)

模拟一个依赖是可能和自然的,但是模拟在方法体中声明的对象并不自然而且很棘手。

我想象3个解决方案:

1)为什么不是简单地断言文件是用预期的字符写的,而不是嘲笑?

它可以避免欺骗,但如果您经常执行此任务并且想要对它们进行单元测试,则可能会多余且缓慢。

2)使局部变量成为实例字段来模拟它。这似乎真的不是一个干净的解决方案。如果在同一个类中有多个方法执行此类处理,则可能会重复使用同一个编写器或具有多个编写器字段。在这两种情况下,您都可能有副作用。

3)如果你执行了许多写操作并且想要真正隔离对编写器的调用,那么你就有了一个解决方案:重新设计你的代码以获得一个可测试的类。

您可以提取依赖项以执行编写器处理。该类可以提供具有执行指令所需参数的方法。我们可以称之为:WriteService

 public class WriteService {
    ...
    public void writeAndClose(Writer writer, String message){
      try {   
        writer.write(message);
        writer.close();
       } 
        catch (IOException e) {
        throw new IndexFileInitializationException("Error initialization index  file " + emptyIndexFile.getPath());
       }
     }
 }

此类是可测试的,因为writer依赖项是一个参数。

你打电话给新服务:

public class YourAppClass{

  private WriteService writeService;

  public YourAppClass(WriteService writeService){
    this.writeService=writeService;
  }

  @Override
  public void initIndexFile(File emptyIndexFile) {
        Writer writer = new FileWriter(emptyIndexFile);
        writeService.writeAndClose(writer,"[]");
  }
}

现在initIndexFile()也可以通过模拟WriteService来测试。 您可以检查在writeService上使用good参数调用writeAndClose()。

就个人而言,我会使用第一种解决方案或第三种解决方案。