【问题标题】:Spring Boot Unit TestingSpring Boot 单元测试
【发布时间】:2019-06-05 22:18:44
【问题描述】:

我目前正在 Spring Boot 中开发 Rest 服务,并正在尝试开发单元测试。我的 Rest 控制器接受请求并利用在另一个包中定义的 Sftp 连接器直接与我们的服务器交互。我已经定义了我的测试用例来模拟 Sftp 连接器的行为,而不是让它在模拟数据被馈送到 Rest 端点时执行。在测试期间,我收到了大量错误日志,表明 Sftp 连接器引发了异常。如果发送到端点的模拟数据仍被馈送到连接器类中,这是有道理的。我猜我没有正确地模拟连接器。到目前为止,我一直未能找到模拟连接器的方法。这可能吗?或者我可以只模拟与测试类在同一个包中定义的类吗?

控制器:

package RestAPI;

import JSch.SFTP.SftpConnector;
import JSch.SFTP.fileInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;


@RestController
@Api( value = "Test Rest Controller", description = "Basic test for SFTP 
connector with very basic operations.")
public class SftpRestController {

    private Properties getProperties(){ //used to get connection values from 
 external file
    InputStream input = null;
    Properties prop = new Properties(); //new property
    try{
        if(System.getProperty("os.name").contains("Windows")){
            input = new FileInputStream("C:\\tmp\\application.properties"); //get resources stream
        }
        else{
            input = new FileInputStream("application.properties");
        }
        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
        String line;
        while((line = reader.readLine()) != null){
            String[] split = line.split("=");
            prop.put(split[0], split[1]);
        }

    }catch(IOException IO){
        IO.printStackTrace();
    }finally{
        if(input != null )
            try{
                input.close(); //close the stream
            }catch(IOException IO2){
                IO2.printStackTrace();
            }
    }
    System.out.println(prop.getProperty("sftp.port"));
    return prop;
}

@ApiOperation(value = "Check that service is up and running.", response = String.class)
@RequestMapping("/")
public @ResponseBody String index(){ return "Rest Test Service is up and running";}


@ApiOperation(value = "Upload a file", response = String.class)
@PostMapping(value="/Upload") //uploads files so long as the files are defined in one structure in the form body
public @ResponseBody String multipartUpload(@RequestBody MultipartFile[] files, @RequestParam("dest") String dest){
    String fileMessage = ""; //need something to send back to the caller
    SftpConnector connector = new SftpConnector(this.getProperties()); //plugs properties into the constructor and opens a connection
    for( int i = 0; i < files.length; i++ ){ //for each file uploaded
        connector.uploadFile(files[i], dest); //call the upload method with the file reference and where it's going
        fileMessage = fileMessage + files[i].getOriginalFilename() + " has been uploaded to remote directory \n";
    }
    connector.closeSFTPConnection(); //manually close the connection
    return fileMessage;
}

@ApiOperation(value = "Downloads a file over SFTP and writes it to local file system specified", response = String.class)
@GetMapping(value="/Download")
public @ResponseBody String downloadFile(@RequestParam("fileName") String fName, @RequestParam("dest") String dest){
    SftpConnector connector = new SftpConnector(this.getProperties()); //new connector
    connector.downloadFile(fName, dest); //pull the specified file down
    connector.closeSFTPConnection(); //close the connection
    return fName + " has been downloaded to the specified local directory";
}

@ApiOperation(value = "Moves all files on a remote server from one directory to another based on file type specified", response = String.class)
@PutMapping(value="/moveFiles")
public @ResponseBody String moveFiles(@RequestParam("dir") String origin, @RequestParam("fileType") String type,@RequestParam("dest") String dest){
    SftpConnector connector = new SftpConnector(this.getProperties());
    connector.moveFiles(origin, type, dest); //moves a group of files based file type from one directory to another
    connector.closeSFTPConnection();
    return "All files have been moved.";
}

@ApiOperation(value = "Moves a single specific file", response = String.class)
@GetMapping(value="/moveFile")
public @ResponseBody String moveFile(@RequestParam("dir") String origFile, @RequestParam("dest") String dest){
    SftpConnector connector = new SftpConnector(this.getProperties());
    connector.moveFile(origFile, dest); //moves a single file.  file name must be specified.
    connector.closeSFTPConnection();
    return FilenameUtils.getName(origFile) + " has been moved to " + dest;
}

@ApiOperation(value = "Gets a specified file stream and returns it as a string", response = String.class)
@GetMapping(value="/readFile")
public @ResponseBody String readFile(@RequestParam("path") String path){
    String fileContents;
    try{
        SftpConnector connector = new SftpConnector(this.getProperties());
        InputStream fis = connector.readFile(path); //gets an open file stream
        fileContents = IOUtils.toString(fis,"UTF-8"); //takes a file stream, reads into variable in specified encoding
        fis.close(); //closes the stream
        connector.closeSFTPConnection();
    }catch(IOException IO){
        fileContents = IO.toString();
    }
    return fileContents;
}

@ApiOperation(value="Returns a list of file names as a string", response = String.class)
@GetMapping(value="/listFiles")
public @ResponseBody String fileNames(@RequestParam("dir") String origin, @RequestParam("fileType") String type){
    String base = "";
    try{
        SftpConnector connector = new SftpConnector(this.getProperties());
        Collection<fileInfo> files = connector.listFiles(origin, type); //gets a list of files with name, type, and an input stream
        Iterator<fileInfo> iterator = files.iterator(); //iterator to roll over collection
        while(iterator.hasNext()){ //while not empty
            fileInfo thisFile = iterator.next(); //get the next fileInfo
            base = base + thisFile.getName() + '\n' ; //write the name to the base string
            thisFile.getFileStream().close(); //close the file stream
        }
        connector.closeSFTPConnection();
    }catch(Exception ex){
        ex.printStackTrace();
    }
    return base;
}

@ApiOperation(value = "Moves a file by the connectors readFile and writeFile methods rather than SFTP rename", response = String.class)
@GetMapping(value="/test")
public @ResponseBody String StreamTest(@RequestParam("file") String file, @RequestParam("dest") String dest){
    SftpConnector connector = new SftpConnector(this.getProperties());
    connector.writeFile(dest, FilenameUtils.getName(file), connector.readFile(file));
    connector.closeSFTPConnection();
    return file + " has been successfully read, and written to destination";
}
}

Sftp 连接器:

package JSch.SFTP;

import com.jcraft.jsch.*;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.*;

@Component
public class SftpConnector {
private String sftpHost;
private String sftpUser;
private int sftpPort;
private String sftpPass;
private Session session = null;
private ChannelSftp channelSftp = null;

public String getSftpHost() {
    return sftpHost;
}

public void setSftpHost(String sftpHost) {
    this.sftpHost = sftpHost;
}

public String getSftpUser() {
    return sftpUser;
}

public void setSftpUser(String sftpUser) {
    this.sftpUser = sftpUser;
}

public int getSftpPort() {
    return sftpPort;
}

public void setSftpPort(int sftpPort) {
    this.sftpPort = sftpPort;
}

public String getSftpPass() {
    return sftpPass;
}

public void setSftpPass(String sftpPass) {
    this.sftpPass = sftpPass;
}

public void setConnectionVars(String host, String user, String password, int port){
    this.setSftpHost(host);
    this.setSftpPort(port);
    this.setSftpUser(user);
    this.setSftpPass(password);
}

public SftpConnector(Properties prop){
    this.setConnectionVars(prop.getProperty("sftp.host"), prop.getProperty("sftp.user"), prop.getProperty("sftp.pass"), Integer.parseInt(prop.getProperty("sftp.port"))); //set the connection variables
    this.openSFTPConnection(); //open the sftp connection
}

public Session getSession() {
    return session;
}

public void setSession(Session session) {
    this.session = session;
}

public ChannelSftp getChannelSftp() {
    return channelSftp;
}

public void setChannelSftp(ChannelSftp channelSftp) {
    this.channelSftp = channelSftp;
}

public void openSFTPConnection(){
    try{
        JSch jsch = new JSch(); //create the new JSch var
        Session baseSession = jsch.getSession(this.getSftpUser(), this.getSftpHost(),this.getSftpPort()); //establish the ssh session info
        baseSession.setPassword(this.getSftpPass()); //auth over password
        baseSession.setConfig("StrictHostKeyChecking","no"); // don't require hosting key check will need to change after testing
        baseSession.setConfig("PreferredAuthentications","password"); //used to omit kerberos authentication
        this.setSession(baseSession); //set the session to private variable
        this.getSession().connect(); //open the session
        ChannelSftp tempChannel = (ChannelSftp) this.getSession().openChannel("sftp"); //create an SFTP channel
        this.setChannelSftp(tempChannel); //set the channel to the private variable
        this.getChannelSftp().connect(); //open the SFTP connection
    }catch(JSchException e){
        e.printStackTrace();
    }
}

public void closeSFTPConnection(){
    try{
        if(this.getChannelSftp().isConnected()){ //if the channel is open so must be the session close both
            this.getChannelSftp().disconnect();
            this.getSession().disconnect();
        }else{ //if the channel is closed check to see if the session is still open
            if(this.getSession().isConnected()){
                this.getSession().disconnect();
            }
        }
    }catch(Exception e){
        e.printStackTrace();
    }
}

public Collection<fileInfo> listFiles(String dir, String filter){
    Collection<fileInfo> files = new ArrayList<fileInfo>();
    try{
        if(this.getChannelSftp() == null || !this.getChannelSftp().isConnected())     //make sure that the JSch sessions been opened.
            this.openSFTPConnection();
        this.getChannelSftp().cd(dir); //set the working directory
        Vector<ChannelSftp.LsEntry> fileList = this.getChannelSftp().ls(filter); //Get a listing of the files in the directory depending on a filter
        Iterator<ChannelSftp.LsEntry> iterator = fileList.iterator(); //iterate over the collection
        while(iterator.hasNext()) {
            ChannelSftp.LsEntry entry = iterator.next();
            fileInfo thisFile = new fileInfo(entry.getFilename(), entry.getAttrs().getAtimeString(), FilenameUtils.getExtension(entry.getFilename()), this.getChannelSftp().get(entry.getFilename())); //get a buffered input stream for each file.
            files.add(thisFile);
        }
    }catch(SftpException e){
        e.printStackTrace();
    }
    return files;
}

public InputStream readFile(String filePath){ //built to read get a file stream from a given file path
    InputStream inputStream = null;
    try{
        if(this.getChannelSftp() == null || !this.getChannelSftp().isConnected()) //if the channel isn't open, open it.
            this.openSFTPConnection(); //open connection
        inputStream = this.getChannelSftp().get(filePath); //get the stream

    }catch(SftpException ex){
        ex.printStackTrace();
    }
    return inputStream;
}

public String uploadFile(MultipartFile file, String dest){
    try{
        System.out.println(this.getSession().getConfig("FileUploadBaseSizeLimit"));
        if(this.getChannelSftp() == null || !this.getChannelSftp().isConnected()) //if not connected open the connection
            this.openSFTPConnection();
        this.getChannelSftp().cd(dest); //set working dir
        this.getChannelSftp().put(file.getInputStream(), dest + '/' + file.getOriginalFilename()); //get the input stream from the multipart file and put it in the destination
    }catch(IOException IO){
        IO.printStackTrace();
    }catch(SftpException sftp){
        sftp.printStackTrace();
    }
    return file.getOriginalFilename() + " has been uploaded";
}

public String downloadFile(String orig, String dest){
    try{
        if(this.getChannelSftp() == null || !this.getChannelSftp().isConnected()) //if not connected
            this.openSFTPConnection();
        File download = new File(dest + '/' + FilenameUtils.getName(orig)); //create a new local file
        OutputStream outputStream = new FileOutputStream(download);  //generate an output stream to the file
        byte[] buffer = new byte[10000000]; //10MB buffer instance
        BufferedInputStream bufferedInputStream = new BufferedInputStream(this.getChannelSftp().get(orig)); //create a buffered input stream off the input stream from the ssh get
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); // and a buffered output stream off the output stream we created
        int readCount; //integer link to the buffer
        while((readCount = bufferedInputStream.read(buffer)) > 0){ //read the buffered input stream into the buffer.  while it's not empty...
            bufferedOutputStream.write(buffer, 0, readCount); //write the buffer to the output stream
        }
        bufferedInputStream.close(); //close the streams
        bufferedOutputStream.close();
        outputStream.close();

    }catch(IOException IO){
        IO.printStackTrace();
    }catch(SftpException sftp){
        sftp.printStackTrace();
    }
    return "File " + FilenameUtils.getName(orig)  + " has been downloaded."; //return that we've successfully uploaded the file
}

public void writeFile(String dir, String fileName, InputStream fileIS) { //writes a file given the destination directory, fileName, and an input stream
    try{
        if(this.getChannelSftp() == null || !this.getChannelSftp().isConnected() ) //if the connection isn't already open, open it
            this.openSFTPConnection();
        this.getChannelSftp().cd(dir); //set the working dir for better shorthand
        this.getChannelSftp().put(fileIS, fileName); //put the file.
        fileIS.close(); //close the file stream

    }catch(SftpException e){
        e.printStackTrace();
    }catch(IOException IO){
        IO.printStackTrace();
    }

}

public void moveFile(String path, String dest){
    try{
        if(this.getChannelSftp() == null || !this.getChannelSftp().isConnected()){ //open connection if not done already
            this.openSFTPConnection();
        }
        this.getChannelSftp().rename(path, dest + '/' + FilenameUtils.getName(path)); //ssh command to move the file.
    }catch(SftpException e){
        e.printStackTrace();
    }
}

public String moveFiles(String dir, String filter, String dest){
    try{
        if(this.getChannelSftp() == null || !this.getChannelSftp().isConnected()){ //open if not done already
            this.openSFTPConnection();
        }
        Collection<fileInfo> files = this.listFiles(dir, filter); //invoke the listFiles method and convert to an array
        Iterator<fileInfo> iterator = files.iterator(); //iterator
        while( iterator.hasNext()){ //while there is another value in the collection
            fileInfo thisFile = iterator.next(); // have to store the fileInfo somewhere.  can't just keep calling next.
            this.getChannelSftp().rename(dir + '/' + thisFile.getName(), dest + '/' + thisFile.getName());
        }
        this.closeSFTPConnection(); //close the connection
    }catch(Exception ex){
        ex.printStackTrace();
    }
    return "all files moved";
}
}

测试类:

package RestAPI;

import JSch.SFTP.SftpConnector;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.io.FileInputStream;
import java.io.InputStream;


@RunWith(SpringRunner.class)
@WebMvcTest(value = SftpRestController.class, secure = false)
public class SftpRestUnitTest {

@Autowired
private MockMvc mockMvc;

@MockBean
SftpConnector connector ;

@Test
public void testFileUpload() throws Exception{
    MockMultipartFile testFile1 = new MockMultipartFile("data", "WSUID1.csv", "application/csv", "some xml".getBytes());
    Mockito.when(
            connector.uploadFile(Mockito.any(),Mockito.anyString())
    ).thenReturn("WSUID1.csv has been uploaded");
    MvcResult result = mockMvc.perform(MockMvcRequestBuilders.multipart("/Upload").file(testFile1).param("dest", "/tmp/test")).andReturn();
    System.out.println(result.getResponse().getContentAsString());
    assert result.getResponse().getContentAsString().equals("WSUID1.csv has been uploaded to remote directory \n");
}
@Test
public void testDownload() throws Exception{
    String expected = "TestFile.csv has been downloaded to the specified local directory";
    Mockito.when(
            connector.downloadFile(Mockito.anyString(), Mockito.anyString())
    ).thenReturn("File TestFile.csv has been downloaded.");
    RequestBuilder request = MockMvcRequestBuilders.get("/Download").param("fileName","/tmp/TestFile.csv").param("dest", "/tmp");
    MvcResult result = mockMvc.perform(request).andReturn();
    System.out.println(result.getResponse().getContentAsString());
    assert result.getResponse().getContentAsString().equals(expected);
}
@Test
public void testMoveFiles() throws Exception{
    Mockito.when(
            connector.moveFiles(Mockito.anyString(),Mockito.anyString(),Mockito.anyString())
    ).thenReturn("all files moved");
    RequestBuilder request = MockMvcRequestBuilders.put("/moveFiles").param("dir","/IB_Test").param("fileType", "*.csv").param("dest", "/InternatlPgms/INTO/Archive/Receipt");
    MvcResult result = mockMvc.perform(request).andReturn();
    System.out.println(result.getResponse().getContentAsString());
    assert result.getResponse().getStatus() == 200;
}
@Test
public void testReadFile() throws Exception{
    String expected = "greetings from java test";
    InputStream mockStream = Mockito.mock(FileInputStream.class);
    Mockito.when(
            connector.readFile(Mockito.anyString())
    ).thenReturn(mockStream);
    RequestBuilder request = MockMvcRequestBuilders.get("/readFile").param("path", "IB_Test");
    MvcResult result = mockMvc.perform(request).andReturn();
    System.out.println(result.getResponse().getContentAsString());
    assert result.equals(expected);
}
}

错误日志:

The specified file is a directory.
at com.jcraft.jsch.ChannelSftp.throwStatusError(ChannelSftp.java:2873)
at com.jcraft.jsch.ChannelSftp.get(ChannelSftp.java:1337)
at com.jcraft.jsch.ChannelSftp.get(ChannelSftp.java:1290)
at JSch.SFTP.SftpConnector.readFile(SftpConnector.java:133)
at RestAPI.SftpRestController.readFile(SftpRestController.java:104)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:71)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:166)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

非常感谢任何帮助。

【问题讨论】:

    标签: java spring unit-testing spring-boot


    【解决方案1】:

    您在控制器的每个方法中创建一个新的 SftpConnector 实例。它不是注入的 bean,因此不会被您的模拟替换。

    例如,看看这篇文章(第 5 节): https://www.baeldung.com/spring-boot-testing

    【讨论】:

    • 因此,如果我将单个连接器定义为私有最终 SftpConnector 连接器; @Autowired public SftpRestController(){ this.connector = new SftpConnector(getProperties()); } 这应该允许我嘲笑它的行为是否正确?
    • 不完全是。您必须注入您的依赖项,即 SftpController,才能在测试中用模拟替换它。我将编辑答案以添加相关资源。
    • 我添加了示例中列出的两个接口,一个是服务层实现,另一个是框架。我已将服务层接口作为 bean 添加到测试配置静态类中。我已经自动装配了服务层,并使用 MockBean 注释声明了一个新的 SftpConnector。没有骰子。它仍在尝试连接到外部系统并执行操作。有没有办法模拟这种行为?
    • 在浏览了 Mockitos 文档之后,似乎无法模拟 JSch 会话。为了模拟 aftp 连接,我需要模拟 ssh 服务器并归档一个系统。感谢您的来信。
    • 问题是你不需要模拟连接。您需要模拟 SftpConnector 类,我相信它是由您提供的。如果你正确地将它注入你的控制器,应该可以在你的测试中注入一个模拟而不是真正的实现。你提到你做了一些自动装配,但没有代码示例,我无法判断你这样做是对还是错。