首页 > 解决方案 > 用于 Soap Web 服务的 JUnit 5 测试 - ParameterResolutionException:未注册 ParameterResolver

问题描述

我正在尝试使用 Java 1.8 为已发布的 .NET Soap Web 服务创建 JUnit 5 测试。

目前,我正在遵循代码库中 WebServiceClient 的模式,其中包含一个main()方法。


WebServiceClient.java:

import static com.google.common.base.Preconditions.checkNotNull;

public class WebServiceClient {
    
    IPersonWebService service;

    public WebServiceClient(IPersonWebService service) {
        this.service = checkNotNull(service);
    }

    private PersonData getPersonData() throws Exception {
        return service.getPersons(null);
    }

    public static void main(String [] args) {
        IPersonWebService service = new IPersonWebServiceImpl().getBasicHttpBindingIPersonWebServiceImpl();

        WebServiceClient client = new WebServiceClient(service);
        PersonData personData = client.getPersonData();
        System.out.println(personData.toString());
    }
}


需要遵循相同类型的功能:

WebServiceTest.java:

import org.junit.Before;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static com.google.common.base.Preconditions.checkNotNull;

public class WebServiceTest {

    IPersonWebService service;

    public WebServiceTest(IPersonWebService service) {
        this.service = checkNotNull(service);
    }

    @Before
    public void before(IPersonWebService service) {
        this.service = checkNotNull(service);
    }

    @Test
    public void testGetPersonData() throws Exception {
        IPersonWebService service =
                new IPersonWebServiceImpl().getBasicHttpBindingIPersonWebServiceImpl();

        WebServiceTest client = new WebServiceTest(service);
        PersonData personData = client.getPersonData();
        assertThat(personData).isNotNull();

    }

    private PersonData getPersonData() throws Exception {
        return service.getPersonData(null);
    }
}

在 IntelliJ IDEA 中运行它会导致:

org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [com.myapp.IPersonWebService arg0] in constructor [public com.myapp.WebServiceTest(com.myapp.IPersonWebService)].
    at java.util.Optional.orElseGet(Optional.java:267)
    at java.util.ArrayList.forEach(ArrayList.java:1249)
    at java.util.ArrayList.forEach(ArrayList.java:1249)

IPersonWebService.java:

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.ws.RequestWrapper;
import javax.xml.ws.ResponseWrapper;

/**
 * This class was generated by the JAX-WS RI.
 * JAX-WS RI 2.2.9-b130926.1035
 * Generated source version: 2.2
 * 
 */
@WebService(name = "IPersonWebService", targetNamespace = "http://sample.org/")
@XmlSeeAlso({
    ObjectFactory.class
})
public interface IPersonWebService {

    @WebMethod(operationName = "GetPersonData", 
               action = "http://sample.org/IPersonWebService/GetPersonData")
    @WebResult(name = "GetVehiclesResult", 
               targetNamespace = "http://sample.org/")
    @RequestWrapper(localName = "GetPersonData", 
                    targetNamespace = "http://sample.org/", 
                    className = "com.myapp.GetPersonData")
    @ResponseWrapper(localName = "GetPersonDataResponse", 
                     targetNamespace = "http://sample.org/", 
                     className = "com.myapp.GetPersonDataResponse")
    public {PersonData} getPersonData(
            @WebParam(name = "applicationID", 
                      targetNamespace = "http://sample.org/")
                      String applicationID)
        throws IPersonWebServicetExceptionFaultMessage;
}


这包含将被导入内存的实际 WSDL。

IPersonWebServiceImpl.java:

import java.net.MalformedURLException;
import java.net.URL;

import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.WebEndpoint;
import javax.xml.ws.WebServiceClient;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.WebServiceFeature;

/**
 * This class was generated by the JAX-WS RI.
 * JAX-WS RI 2.2.9-b130926.1035
 * Generated source version: 2.2
 *
 */
@WebServiceClient(name = "IPersonWebServiceImpl", 
                  targetNamespace = "http://sample.org/", 
                  wsdlLocation = "https://sample.com/WCF/IPersonWebServiceImpl.svc?singleWsdl")
public class IPersonWebServiceImpl
    extends Service
{

    private final static URL IPersonWebServiceImpl_WSDL_LOCATION;
    private final static WebServiceException IPersonWebServiceImpl_EXCEPTION;
    private final static QName IPersonWebServiceImpl_QNAME = new QName("http://sample.org/", "IPersonWebServiceImpl");

    static {
        URL url = null;
        WebServiceException e = null;
        try {
            url = new URL("https://sample.com/WCF/IPersonWebServiceImpl.svc?singleWsdl");
        } catch (MalformedURLException ex) {
            e = new WebServiceException(ex);
        }
        IPersonWebServiceImpl_WSDL_LOCATION = url;
        IPersonWebServiceImpl_EXCEPTION = e;
    }

    public IPersonWebServiceImpl() {
        super(__getWsdlLocation(), IPersonWebServiceImpl_QNAME);
    }

    public IPersonWebServiceImpl(final WebServiceFeature... features) {
        super(__getWsdlLocation(), IPersonWebServiceImpl_QNAME, features);
    }

    public IPersonWebServiceImpl(final URL wsdlLocation) {
        super(wsdlLocation, IPersonWebServiceImpl_QNAME);
    }

    public IPersonWebServiceImpl(final URL wsdlLocation, 
                                 final WebServiceFeature... features) {
        super(wsdlLocation, IPersonWebServiceImpl_QNAME, features);
    }

    public IPersonWebServiceImpl(final URL wsdlLocation, 
                                 final QName serviceName) {
        super(wsdlLocation, serviceName);
    }

    public IPersonWebServiceImpl(final URL wsdlLocation, 
                                 final QName serviceName, 
                                 final WebServiceFeature... features) {
        super(wsdlLocation, serviceName, features);
    }

    @WebEndpoint(name = "BasicHttpBinding_IPersonWebServiceImpl")
    public IPersonWebServiceImpl getBasicHttpBindingIPersonWebServiceImpl() {
        return super.getPort(new QName("http://sample.org/", 
                                       "BasicHttpBinding_IPersonWebServiceImpl"), 
                                       IPersonWebServiceImpl.class);
    }

    @WebEndpoint(name = "BasicHttpBinding_IPersonWebServiceImpl")
    public IPersonWebServiceImpl getBasicHttpBindingIPersonWebServiceImpl(final WebServiceFeature... features) {
        return super.getPort(new QName("http://sample.org/", 
                                       "BasicHttpBinding_IPersonWebServiceImpl"), 
                                       IPersonWebServiceImpl.class, features);
    }

    private static URL __getWsdlLocation() {
        if (IPersonWebServiceImpl_EXCEPTION!= null) {
            throw IPersonWebServiceImpl_EXCEPTION;
        }
        return IPersonWebServiceImpl_WSDL_LOCATION;
    }
}

问题):

org.junit.jupiter.api.extension.ParameterResolutionException: 
    No ParameterResolver registered for parameter...

如何解决这个问题?


WebServiceClient.java工作并显示从IPersonWebServiceImpl.java(请注意这是如何在静态子句中显式设置 WSDL )获得的PersonData。

来自 RESTful Web 服务背景,而不是基于 SOAP 的背景,因此任何建议都将不胜感激。

标签: javaweb-servicessoapjunitjunit5

解决方案


因此,首先,感谢您提供大量代码并明确您的问题。我知道你问已经几个月了,但我会尽力帮助的。

我认为有两个问题:异常(tldr 这是一个junit版本冲突),以及用于编写单元测试的结构化代码。

参数解析异常

@Test从 JUnit5 导入,又名“junit-jupiter”,但@Before来自 JUnit4,又名“junit-vintage”或“junit”。

import org.junit.Before; // junit4
import org.junit.jupiter.api.BeforeAll; // junit5
import org.junit.jupiter.api.Test; // junit5

因为@Test来自 junit-jupiter,所以测试使用 JUnit5 运行,它(与 JUnit4 不同)允许在测试和构造函数方法中使用参数。WebServiceTest的构造函数需要一个参数。但是您没有提供程序IPersonWebService,因此 JUnit5 中断 - 它无法解决它。

所以要解决这个问题:

  • 删除构造函数
  • before方法中删除参数
  • 更改@Before@BeforeEach(并修复导入)

我还建议(如果可能的话)排除您可能添加的任何 JUnit4 依赖项(junit-vintage为了向后兼容),或者可能已经从其他依赖项中偷偷加入。它可以防止这样的混淆。

例如,使用 maven,要查看有哪些 JUnit 版本及其来源,查看和过滤依赖关系树:

mvn dependency:tree -Dincludes="*junit*"

编写单元测试

我不确定该应用程序的用途是什么,但是(正如我认为您已经发现的那样)它很难测试。这对我来说确实很奇怪。WebServiceClient有一个 main 方法(我猜它会被直接调用?),它会立即创建 API 并调用它并返回结果——因此“业务逻辑”和“api”之间没有分离。也许那个主要方法是用来演示的?

最好把事情分开。希望它使测试更加清晰。


让我们做一个专门的WebServiceClient. 这应该是唯一与 交互的类IPersonWebService

这里不应该有任何花哨的逻辑,只是带有基本异常处理的简单 API 调用。


// import generated soap stuff

public class WebServiceClient {

  private final IPersonWebService service;

  public WebServiceClient(IPersonWebService service) {
    // an instance of the API is received in the constructor
    this.service = service;
  }

  private PersonsData getPersonData() throws WebServiceClientException {
    try {
      // call the API
      PersonsData persons = service.getPersonData(null);
      if (personsData == null) {
        throw new WebServiceClientException("Received null response from PersonsData :(");
      }
      return persons;
    } catch (IPersonWebServicetExceptionFaultMessage e) {
      // wrap the SOAP exception in our own wrapper
      // it's best not to let SOAP code spread into our project.
      throw new WebServiceClientException("Tried fetching PersonsData, but got exception from API", e);
    }
  }
}

这是该服务的测试类。

每种@Test方法都完全独立于其他方法,它有自己的小气泡来保持测试独立。

我真的建议使用 Mockito 之类的模拟框架来创建虚拟实例。它真的很强大。在这种情况下,这意味着我们可以轻松地测试异常。Mockito 3与 JUnit5 配合得非常好——我在这里使用了参数注入来为测试制作模拟(Mockito 在后台提供了一个参数解析器)。

@ExtendWith(MockitoExtension.class)
class WebServiceClientTest {

  @Test
  public void testWebServiceClientCreated(
      // it's not actually important about the internal workings of IPersonWebService
      // so use mockito to mock it!
      // all we care about is that if it's there, then WebServiceClient can be created
      @Mock
          IPersonWebService service) {
    WebServiceClient wsc = new WebServiceClient(service);
    assertNotNull(wsc);
  }

  // test happy flow
  @Test
  public void testPersonsDataReturned(
      // again, we don't care about the internals of IPersonWebService, just that it returns data
      // so mock it!
      @Mock
      IPersonWebService service,

      // it's also not important about the internals of PersonsData,
      // just that it's an object that's returned
      @Mock
      PersonsData personsDataMock) {
    // set up the mock
    when(service.getPersonsData(isNull())).thenReturn(personsDataMock);

    WebServiceClient wsc = new WebServiceClient(service);

    // we don't need to check WebServiceClient here! We already have a test that does this.
    // each test should only focus on one thing
    // assertNotNull(wsc);

    PersonsData result = wsc.getPersonsData();

    // now we can check the result
    assertNotNull(result);
    assertEquals(personsDataMock, result);
  }

  // again, testing for failure is much more interesting than happy flows
  // in this case, the test will fail!
  // we'd better fix WebServiceClient to handle unexpected exceptions from IPersonWebService
  @Test
  public void testWhenApiThrowsNullPointerExceptionExpectWebServiceClientException(
      @Mock
      IPersonWebService service) {
    // mock throwing an exception
    NullPointerException npe = new NullPointerException("Another one of those weird external webservice exceptions");
    doThrow()
        .when(service.getPersonsData(isNull()));

    WebServiceClient wsc = new WebServiceClient(service);
    
    WebServiceClientException thrownException = assertThrows(WebServiceClientException.class,
        () -> wsc.getPersonsData()
    );

    // now we can check the result
    assertNotNull(thrownException);
    assertEquals("Tried fetching PersonsData, but got exception from API", thrownException.getMessage());
    assertEquals(npe, thrownException.getCause());
  }
}

并且是处理 API 时任何异常的一个很好的包装器。

class WebServiceClientException extends Exception {

  public WebServiceClientException(String message) {
    super(message);
  }

  public WebServiceClientException(String message, Exception cause) {
    super(message, cause);
  }
}

推荐阅读