Mocking HttpClient ในการทดสอบหน่วย


112

ฉันมีปัญหาบางอย่างในการพยายามตัดรหัสเพื่อใช้ในการทดสอบหน่วย ประเด็นคือเรื่องนี้ ฉันมีอินเทอร์เฟซ IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

และคลาสที่ใช้ HttpHandler:

public class HttpHandler : IHttpHandler
{
    public HttpClient client
    {
        get
        {
            return new HttpClient();
        }
    }
}

จากนั้นคลาส Connection ซึ่งใช้ simpleIOC เพื่อฉีดการใช้งานไคลเอ็นต์:

public class Connection
{
    private IHttpHandler _httpClient;

    public Connection(IHttpHandler httpClient)
    {
        _httpClient = httpClient;
    }
}

จากนั้นฉันมีโครงการทดสอบหน่วยที่มีคลาสนี้:

private IHttpHandler _httpClient;

[TestMethod]
public void TestMockConnection()
{
    var client = new Connection(_httpClient);

    client.doSomething();  

    // Here I want to somehow create a mock instance of the http client
    // Instead of the real one. How Should I approach this?     

}

เห็นได้ชัดว่าตอนนี้ฉันจะมีเมธอดในคลาส Connection ที่จะดึงข้อมูล (JSON) จากส่วนหลังของฉัน อย่างไรก็ตามฉันต้องการเขียนการทดสอบหน่วยสำหรับชั้นเรียนนี้และเห็นได้ชัดว่าฉันไม่ต้องการเขียนแบบทดสอบเทียบกับส่วนหลังจริง แต่เป็นการล้อเลียน ฉันพยายามหาคำตอบที่ดีให้กับ Google โดยไม่ประสบความสำเร็จอย่างมาก ฉันสามารถและเคยใช้ Moq เพื่อเยาะเย้ยมาก่อน แต่ไม่เคยใช้อะไรเช่น httpClient ฉันควรแก้ไขปัญหานี้อย่างไร?

ขอบคุณล่วงหน้า.


1
การเปิดเผยHttpClientในอินเทอร์เฟซของคุณคือจุดที่มีปัญหา คุณกำลังบังคับให้ลูกค้าของคุณใช้HttpClientคลาสคอนกรีต แต่คุณควรเปิดเผยสิ่งที่เป็นนามธรรมของไฟล์HttpClient.
Mike Eason

ช่วยอธิบายเชิงลึกอีกนิดได้ไหม ฉันจะสร้างตัวสร้างคลาสการเชื่อมต่อได้อย่างไรเพราะฉันไม่ต้องการให้ HttpClient ในคลาสอื่น ๆ ใช้คลาส Connection ตัวอย่างเช่นฉันไม่ต้องการส่ง Concerete HttpClient ในคอนสตรัคเตอร์ของ Connection เพราะจะทำให้คลาสอื่น ๆ ที่ใช้ Connection ขึ้นอยู่กับ HttpClient?
tjugg

Google ไม่สนใจอะไร เห็นได้ชัดว่า mockhttp สามารถใช้การปรับปรุง SEO บางอย่างได้
Richard Szalay

@ ไมค์ - ตามที่กล่าวไว้ในคำตอบของฉันไม่จำเป็นต้องนามธรรม HttpClient สามารถทดสอบได้อย่างสมบูรณ์แบบตามที่เป็นอยู่ ฉันมีโครงการมากมายที่มีชุดทดสอบแบ็กเอนด์โดยใช้วิธีนี้
Richard Szalay

คำตอบ:


37

อินเทอร์เฟซของคุณแสดงHttpClientคลาสที่เป็นรูปธรรมดังนั้นคลาสใด ๆ ที่ใช้อินเทอร์เฟซนี้จะเชื่อมโยงกับอินเทอร์เฟซนี้หมายความว่าไม่สามารถล้อเลียนได้

HttpClientไม่ได้รับมรดกจากอินเทอร์เฟซใด ๆ ดังนั้นคุณจะต้องเขียนของคุณเอง ฉันขอแนะนำรูปแบบเหมือนมัณฑนากร :

public interface IHttpHandler
{
    HttpResponseMessage Get(string url);
    HttpResponseMessage Post(string url, HttpContent content);
    Task<HttpResponseMessage> GetAsync(string url);
    Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}

และชั้นเรียนของคุณจะมีลักษณะดังนี้:

public class HttpClientHandler : IHttpHandler
{
    private HttpClient _client = new HttpClient();

    public HttpResponseMessage Get(string url)
    {
        return GetAsync(url).Result;
    }

    public HttpResponseMessage Post(string url, HttpContent content)
    {
        return PostAsync(url, content).Result;
    }

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await _client.GetAsync(url);
    }

    public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
    {
        return await _client.PostAsync(url, content);
    }
}

ประเด็นในทั้งหมดนี้คือการHttpClientHandlerสร้างของตัวเองHttpClientจากนั้นคุณสามารถสร้างคลาสหลายคลาสที่ใช้งานIHttpHandlerในรูปแบบต่างๆ

ปัญหาหลักของแนวทางนี้คือคุณกำลังเขียนคลาสที่เรียกใช้เมธอดในคลาสอื่นได้อย่างมีประสิทธิภาพอย่างไรก็ตามคุณสามารถสร้างคลาสที่สืบทอดมาจากHttpClient(ดูตัวอย่างของ Nkosiซึ่งเป็นแนวทางที่ดีกว่าของฉันมาก) ชีวิตจะง่ายขึ้นมากหากHttpClientมีอินเทอร์เฟซที่คุณสามารถล้อเลียนได้ แต่น่าเสียดายที่ไม่มี

อย่างไรก็ตามตัวอย่างนี้ไม่ใช่ตั๋วทองคำ IHttpHandlerยังคงอาศัยอยู่HttpResponseMessageซึ่งเป็นของSystem.Net.Httpเนมสเปซดังนั้นหากคุณต้องการการใช้งานอื่น ๆ นอกเหนือจากHttpClientนี้คุณจะต้องทำการแมปบางประเภทเพื่อแปลงการตอบสนองของพวกเขาเป็นHttpResponseMessageวัตถุ หลักสูตรนี้เป็นเพียงปัญหาหากคุณจำเป็นต้องใช้หลายการใช้งานของIHttpHandlerแต่มันดูไม่เหมือนคุณทำเช่นนั้นมันไม่ได้เป็นจุดสิ้นสุดของโลก แต่มันก็เป็นสิ่งที่ต้องคิดเกี่ยวกับ

อย่างไรก็ตามคุณสามารถล้อเลียนได้IHttpHandlerโดยไม่ต้องกังวลเกี่ยวกับHttpClientชั้นเรียนที่เป็นรูปธรรมเนื่องจากมันถูกมองข้ามไป

ฉันขอแนะนำให้ทดสอบวิธีการที่ไม่ใช่แบบอะซิงโครนัสเนื่องจากวิธีเหล่านี้ยังคงเรียกว่าวิธีการอะซิงโครนัส แต่ไม่ต้องกังวลกับการทดสอบหน่วยวิธีการอะซิงโครนัสโปรดดูที่นี่


นี่ตอบคำถามของฉันได้แน่นอน คำตอบของ Nkosis ก็ถูกต้องเช่นกันดังนั้นฉันจึงไม่แน่ใจว่าควรยอมรับคำตอบใดเป็นคำตอบ แต่ฉันจะไปกับข้อนี้ ขอบคุณทั้งสองคนสำหรับความพยายาม
tjugg

@tjugg ดีใจที่ได้ช่วย. อย่าลังเลที่จะโหวตคำตอบหากคุณพบว่ามีประโยชน์
Nkosi

3
น่าสังเกตว่าความแตกต่างหลักระหว่างคำตอบนี้กับ Nkosi คือสิ่งที่เป็นนามธรรมที่บางกว่ามาก Thin น่าจะดีสำหรับวัตถุที่ต่ำต้อย
Ben Aaronson

234

ความสามารถในการขยายของ HttpClient อยู่ในการHttpMessageHandlerส่งผ่านไปยังตัวสร้าง มีจุดประสงค์เพื่อให้สามารถใช้งานเฉพาะแพลตฟอร์มได้ แต่คุณสามารถล้อเลียนได้เช่นกัน ไม่จำเป็นต้องสร้างกระดาษห่อมัณฑนากรสำหรับ HttpClient

หากคุณต้องการ DSL ในการใช้ Moq ฉันมีไลบรารีบน GitHub / Nuget ที่ทำให้ง่ายขึ้นเล็กน้อย: https://github.com/richardszalay/mockhttp

var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

var json = await response.Content.ReadAsStringAsync();

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}

1
ดังนั้นฉันจะส่ง MockHttpMessageHandler เป็นคลาส Httphandler messagehandler? หรือนำไปปฏิบัติในโครงการของตนเองอย่างไร
tjugg

2
คำตอบที่ดีและสิ่งที่ฉันไม่รู้ในตอนแรก ทำให้การทำงานกับ HttpClient ไม่เลวร้ายนัก
Bealer

6
สำหรับผู้ที่ไม่ต้องการจัดการกับการฉีดไคลเอนต์ แต่ยังคงต้องการการทดสอบที่ง่ายมันเป็นเรื่องเล็กน้อยที่จะบรรลุ เพียงแค่เปลี่ยนvar client = new HttpClient()ด้วยvar client = ClientFactory()และการตั้งค่าเขตข้อมูลinternal static Func<HttpClient> ClientFactory = () => new HttpClient();และระดับการทดสอบที่คุณสามารถเขียนข้อมูลนี้
Chris Marisic

3
@ChrisMarisic คุณกำลังแนะนำรูปแบบของสถานบริการเพื่อแทนที่การฉีดยา สถานบริการเป็นรูปแบบการต่อต้านที่รู้จักกันดีดังนั้นจึงเป็นที่นิยมในการฉีดอิมโฮ
MarioDS

2
@MarioDS และไม่ว่าคุณไม่ควรฉีดอินสแตนซ์ HttpClient เลย หากคุณมีความตายตั้งเกี่ยวกับการใช้ฉีด constructer สำหรับเรื่องนี้แล้วคุณควรจะฉีดในขณะที่HttpClientFactory Func<HttpClient>เนื่องจากฉันมองว่า HttpClient เป็นเพียงรายละเอียดการใช้งานเท่านั้นและไม่ใช่การอ้างอิงฉันจะใช้สถิติตามที่แสดงไว้ด้านบน ฉันสบายดีกับการทดสอบที่จัดการกับภายใน ถ้าฉันสนใจเรื่อง pure-ism ฉันจะยืนเซิร์ฟเวอร์เต็มรูปแบบและทดสอบเส้นทางโค้ดสด การใช้การเยาะเย้ยแบบใดก็ตามหมายความว่าคุณยอมรับว่าพฤติกรรมโดยประมาณไม่ใช่พฤติกรรมที่แท้จริง
Chris Marisic

40

ฉันเห็นด้วยกับคำตอบอื่น ๆ ว่าแนวทางที่ดีที่สุดคือการล้อเลียน HttpMessageHandler แทนที่จะห่อ HttpClient คำตอบนี้มีความพิเศษตรงที่ยังคงฉีด HttpClient ทำให้สามารถเป็นซิงเกิลตันหรือจัดการด้วยการฉีดแบบพึ่งพาได้

HttpClient มีวัตถุประสงค์เพื่อสร้างอินสแตนซ์เพียงครั้งเดียวและใช้ซ้ำได้ตลอดอายุของแอปพลิเคชัน

( ที่มา ).

การล้อเลียน HttpMessageHandler อาจเป็นเรื่องยุ่งยากเล็กน้อยเนื่องจาก SendAsync ได้รับการปกป้อง นี่คือตัวอย่างที่สมบูรณ์โดยใช้ xunit และ Moq

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq

namespace MockHttpClient {
    class Program {
        static void Main(string[] args) {
            var analyzer = new SiteAnalyzer(Client);
            var size = analyzer.GetContentSize("http://microsoft.com").Result;
            Console.WriteLine($"Size: {size}");
        }

        private static readonly HttpClient Client = new HttpClient(); // Singleton
    }

    public class SiteAnalyzer {
        public SiteAnalyzer(HttpClient httpClient) {
            _httpClient = httpClient;
        }

        public async Task<int> GetContentSize(string uri)
        {
            var response = await _httpClient.GetAsync( uri );
            var content = await response.Content.ReadAsStringAsync();
            return content.Length;
        }

        private readonly HttpClient _httpClient;
    }

    public class SiteAnalyzerTests {
        [Fact]
        public async void GetContentSizeReturnsCorrectLength() {
            // Arrange
            const string testContent = "test content";
            var mockMessageHandler = new Mock<HttpMessageHandler>();
            mockMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(testContent)
                });
            var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));

            // Act
            var result = await underTest.GetContentSize("http://anyurl");

            // Assert
            Assert.Equal(testContent.Length, result);
        }
    }
}

1
ฉันชอบสิ่งนี้มาก mockMessageHandler.Protected()เป็นฆาตกร ขอบคุณสำหรับตัวอย่างนี้ อนุญาตให้เขียนการทดสอบโดยไม่ต้องแก้ไขแหล่งที่มาเลย
tyrion

1
FYI, Moq 4.8 สนับสนุนการล้อเลียนสมาชิกที่ได้รับการคุ้มครองอย่างรุนแรง - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay

2
นี่ดูดีมาก นอกจากนี้ Moq ยังรองรับ ReturnsAsync ดังนั้นรหัสจึงมีลักษณะดังนี้.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord

ขอบคุณ @kord ฉันเพิ่มคำตอบนั้น
PointZeroTwo

3
มีวิธีใดบ้างที่จะตรวจสอบว่า "SandAsync" ถูกเรียกโดยใช้พารามิเตอร์บางตัว ฉันพยายามใช้ ... Protected () ตรวจสอบ (... ) แต่ดูเหมือนว่าจะใช้ไม่ได้กับวิธี async
Rroman

29

นี่เป็นคำถามที่พบบ่อยและฉันต้องการความสามารถในการล้อเลียน HttpClient แต่ในที่สุดฉันก็ตระหนักได้ว่าคุณไม่ควรเยาะเย้ย HttpClient ดูเหมือนจะมีเหตุผลที่จะทำเช่นนั้น แต่ฉันคิดว่าเราถูกล้างสมองจากสิ่งต่างๆที่เห็นในไลบรารีโอเพนซอร์ส

เรามักจะเห็น "ไคลเอนต์" อยู่ที่นั่นซึ่งเราล้อเลียนในโค้ดของเราเพื่อให้เราสามารถทดสอบแยกกันได้ดังนั้นเราจึงพยายามใช้หลักการเดียวกันนี้กับ HttpClient โดยอัตโนมัติ HttpClient ทำได้มากจริงๆ คุณอาจคิดว่าเป็นผู้จัดการของ HttpMessageHandler ดังนั้นคุณจึงไม่อยากล้อเลียนสิ่งนั้นและนั่นคือสาเหตุที่ยังไม่มีอินเทอร์เฟซ ส่วนที่คุณสนใจจริงๆสำหรับการทดสอบหน่วยหรือการออกแบบบริการของคุณคือ HttpMessageHandler เนื่องจากนั่นคือสิ่งที่ส่งคืนการตอบสนองและคุณสามารถเยาะเย้ยได้

นอกจากนี้ยังควรชี้ให้เห็นว่าคุณควรเริ่มปฏิบัติกับ HttpClient เหมือนข้อตกลงที่ใหญ่กว่า ตัวอย่างเช่นรักษา HttpClients ใหม่ของคุณให้น้อยที่สุด นำกลับมาใช้ใหม่พวกเขาออกแบบมาเพื่อนำกลับมาใช้ใหม่และใช้ทรัพยากรน้อยลงถ้าคุณทำ หากคุณเริ่มปฏิบัติเหมือนเป็นข้อตกลงที่ใหญ่ขึ้นคุณจะรู้สึกผิดมากกว่าที่ต้องการล้อเลียนและตอนนี้ตัวจัดการข้อความจะเริ่มเป็นสิ่งที่คุณกำลังฉีดไม่ใช่ลูกค้า

กล่าวอีกนัยหนึ่งคือออกแบบการอ้างอิงของคุณรอบ ๆ ตัวจัดการแทนที่จะเป็นไคลเอนต์ "บริการ" ที่เป็นนามธรรมที่ดียิ่งกว่านั้นที่ใช้ HttpClient ซึ่งช่วยให้คุณฉีดตัวจัดการและใช้สิ่งนั้นเป็นการพึ่งพาการฉีดของคุณแทน จากนั้นในการทดสอบของคุณคุณสามารถปลอมตัวจัดการเพื่อควบคุมการตอบสนองสำหรับการตั้งค่าการทดสอบของคุณ

การห่อ HttpClient เป็นการเสียเวลาอย่างบ้าคลั่ง

อัปเดต: ดูตัวอย่างของ Joshua Dooms มันเป็นสิ่งที่ฉันแนะนำ


17

ดังที่ได้กล่าวไว้ในความคิดเห็นที่คุณจำเป็นต้องแยกออกไปHttpClientเพื่อที่จะไม่รวมเข้ากับมัน ฉันเคยทำอะไรคล้าย ๆ กันในอดีต ฉันจะพยายามปรับสิ่งที่ฉันทำกับสิ่งที่คุณพยายามทำ

ก่อนอื่นให้ดูที่HttpClientชั้นเรียนและตัดสินใจว่าจะมีฟังก์ชันใดบ้างที่จำเป็น

นี่คือความเป็นไปได้:

public interface IHttpClient {
    System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}

อีกครั้งตามที่ระบุไว้ก่อนหน้านี้เพื่อวัตถุประสงค์เฉพาะ ฉันตัดขาดการพึ่งพาส่วนใหญ่ไปยังทุกสิ่งที่เกี่ยวข้องHttpClientและมุ่งเน้นไปที่สิ่งที่ฉันต้องการกลับมา คุณควรประเมินว่าคุณต้องการสรุปอย่างไรHttpClientเพื่อให้เฉพาะฟังก์ชันที่จำเป็นที่คุณต้องการ

ตอนนี้จะช่วยให้คุณล้อเลียนเฉพาะสิ่งที่จำเป็นในการทดสอบเท่านั้น

ฉันก็จะแนะนำให้ทำไปด้วยความIHttpHandlerสมบูรณ์และใช้สิ่งที่เป็นนามธรรมHttpClient IHttpClientแต่ฉันไม่ได้เลือกเพราะคุณสามารถแทนที่เนื้อหาของอินเทอร์เฟซตัวจัดการของคุณกับสมาชิกของไคลเอนต์ที่เป็นนามธรรมได้

การใช้งานIHttpClientสามารถใช้ในการห่อ / ดัดแปลงของจริง / คอนกรีตHttpClientหรือวัตถุอื่น ๆ สำหรับเรื่องนั้นซึ่งสามารถใช้เพื่อส่งคำขอ HTTP ได้ตามที่คุณต้องการจริงๆคือบริการที่ให้ฟังก์ชันการทำงานดังกล่าวเป็นที่ยอมรับHttpClientโดยเฉพาะ การใช้สิ่งที่เป็นนามธรรมนั้นเป็นวิธีที่สะอาด (ความคิดเห็นของฉัน) และวิธีการที่เป็นของแข็งและสามารถทำให้โค้ดของคุณสามารถบำรุงรักษาได้มากขึ้นหากคุณต้องการเปลี่ยนไคลเอนต์ที่อยู่เบื้องหลังออกไปเป็นอย่างอื่นเมื่อกรอบการทำงานเปลี่ยนไป

นี่คือตัวอย่างของวิธีการติดตั้ง

/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/> 
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
    HttpClient httpClient;

    public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
        httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
    }

    //...other code

     /// <summary>
    ///  Send a GET request to the specified Uri as an asynchronous operation.
    /// </summary>
    /// <typeparam name="T">Response type</typeparam>
    /// <param name="uri">The Uri the request is sent to</param>
    /// <returns></returns>
    public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
        var result = default(T);
        //Try to get content as T
        try {
            //send request and get the response
            var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
            //if there is content in response to deserialize
            if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
                //get the content
                string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                //desrialize it
                result = deserializeJsonToObject<T>(responseBodyAsText);
            }
        } catch (Exception ex) {
            Log.Error(ex);
        }
        return result;
    }

    //...other code
}

ดังที่คุณเห็นในตัวอย่างข้างต้นการยกของหนักจำนวนมากที่มักเกี่ยวข้องกับการใช้งานHttpClientนั้นซ่อนอยู่หลังสิ่งที่เป็นนามธรรม

จากนั้นคลาสการเชื่อมต่อของคุณสามารถฉีดเข้ากับไคลเอนต์ที่เป็นนามธรรมได้

public class Connection
{
    private IHttpClient _httpClient;

    public Connection(IHttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

การทดสอบของคุณสามารถล้อเลียนสิ่งที่จำเป็นสำหรับ SUT ของคุณ

private IHttpClient _httpClient;

[TestMethod]
public void TestMockConnection()
{
    SomeModelObject model = new SomeModelObject();
    var httpClientMock = new Mock<IHttpClient>();
    httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
        .Returns(() => Task.FromResult(model));

    _httpClient = httpClientMock.Object;

    var client = new Connection(_httpClient);

    // Assuming doSomething uses the client to make
    // a request for a model of type SomeModelObject
    client.doSomething();  
}

นี่คือคำตอบ นามธรรมด้านบนHttpClientและอะแดปเตอร์เพื่อสร้างอินสแตนซ์เฉพาะของคุณโดยใช้HttpClientFactory. การทำเช่นนี้ทำให้การทดสอบตรรกะนอกเหนือจากคำขอ HTTP เป็นเรื่องเล็กน้อยซึ่งเป็นเป้าหมายที่นี่
pimbrouwers

13

จากคำตอบอื่น ๆ ฉันขอแนะนำรหัสนี้ซึ่งไม่มีการอ้างอิงภายนอกใด ๆ :

[TestClass]
public class MyTestClass
{
    [TestMethod]
    public async Task MyTestMethod()
    {
        var httpClient = new HttpClient(new MockHttpMessageHandler());

        var content = await httpClient.GetStringAsync("http://some.fake.url");

        Assert.AreEqual("Content as string", content);
    }
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Content as string")
        };

        return await Task.FromResult(responseMessage);
    }
}

4
คุณกำลังทดสอบการเยาะเย้ยอย่างมีประสิทธิภาพ พลังที่แท้จริงของการล้อเลียนคือคุณสามารถตั้งความคาดหวังและปรับเปลี่ยนพฤติกรรมของมันได้ในการทดสอบแต่ละครั้ง ความจริงที่ว่าคุณต้องดำเนินการบางอย่างHttpMessageHandlerด้วยตัวเองทำให้เป็นไปไม่ได้ - และคุณต้องทำเพราะวิธีการprotected internalต่างๆ
MarioDS

3
@MarioDS ฉันคิดว่าประเด็นคือคุณสามารถจำลองการตอบสนองของ HTTP เพื่อให้คุณสามารถทดสอบโค้ดที่เหลือได้ หากคุณฉีดโรงงานที่ได้รับ HttpClient จากนั้นในการทดสอบคุณสามารถจัดหา HttpClient นี้ได้
chris31389

13

ฉันคิดว่าปัญหาคือคุณได้กลับหัวกลับหางเล็กน้อย

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

    public AuroraClient() : this(new HttpClientHandler())
    {
    }

    public AuroraClient(HttpMessageHandler messageHandler)
    {
        _client = new HttpClient(messageHandler);
    }
}

ถ้าคุณดูชั้นเรียนด้านบนฉันคิดว่านี่คือสิ่งที่คุณต้องการ Microsoft ขอแนะนำให้รักษาไคลเอ็นต์ไว้เพื่อประสิทธิภาพที่ดีที่สุดดังนั้นโครงสร้างประเภทนี้จึงช่วยให้คุณทำเช่นนั้นได้ นอกจากนี้ HttpMessageHandler ยังเป็นคลาสนามธรรมจึงสามารถล้อเลียนได้ วิธีการทดสอบของคุณจะมีลักษณะดังนี้:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var mockMessageHandler = new Mock<HttpMessageHandler>();
    // Set up your mock behavior here
    var auroraClient = new AuroraClient(mockMessageHandler.Object);
    // Act
    // Assert
}

สิ่งนี้ช่วยให้คุณทดสอบตรรกะของคุณในขณะที่ล้อเลียนพฤติกรรมของ HttpClient

ขอโทษด้วยหลังจากเขียนสิ่งนี้และลองใช้ด้วยตัวเองฉันรู้ว่าคุณไม่สามารถเยาะเย้ยวิธีการป้องกันบน HttpMessageHandler ได้ ต่อมาฉันได้เพิ่มรหัสต่อไปนี้เพื่อให้สามารถฉีดล้อเลียนที่เหมาะสมได้

public interface IMockHttpMessageHandler
{
    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    private readonly IMockHttpMessageHandler _realMockHandler;

    public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
    {
        _realMockHandler = realMockHandler;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _realMockHandler.SendAsync(request, cancellationToken);
    }
}

การทดสอบที่เขียนด้วยสิ่งนี้จะมีลักษณะดังนี้:

[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
    // Arrange
    var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
    // Set up Mock behavior here.
    var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
    // Act
    // Assert
}

9

เพื่อนร่วมงานคนหนึ่งของฉันสังเกตเห็นว่าวิธีการส่วนใหญ่HttpClientทั้งหมดเรียกใช้SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)ภายใต้ประทุนซึ่งเป็นวิธีการเสมือนจริงจากHttpMessageInvoker:

วิธีที่ง่ายที่สุดในการเยาะเย้ยHttpClientคือเพียงแค่ล้อเลียนวิธีการนั้น:

var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);

และรหัสของคุณสามารถเรียกใช้HttpClientเมธอดคลาสได้มากที่สุด (แต่ไม่ใช่ทั้งหมด) รวมถึงปกติ

httpClient.SendAsync(req)

ตรวจสอบที่นี่เพื่อยืนยัน https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs


1
สิ่งนี้ใช้ไม่ได้กับรหัสใด ๆ ที่โทรSendAsync(HttpRequestMessage)โดยตรง หากคุณสามารถแก้ไขโค้ดของคุณไม่ให้ใช้ฟังก์ชันอำนวยความสะดวกนี้ได้การเยาะเย้ย HttpClient โดยตรงโดยการลบล้างSendAsyncเป็นวิธีแก้ปัญหาที่สะอาดที่สุดที่ฉันพบ
Dylan Nicholson

8

อีกทางเลือกหนึ่งคือการตั้งค่าเซิร์ฟเวอร์ HTTP ที่ไม่สมบูรณ์ซึ่งส่งคืนการตอบกลับสำเร็จรูปตามรูปแบบที่ตรงกับ URL คำขอซึ่งหมายความว่าคุณทดสอบคำขอ HTTP จริงไม่ใช่การล้อเลียน ในอดีตสิ่งนี้จะต้องใช้ความพยายามอย่างมีนัยสำคัญและจะต้องพิจารณาให้ช้าลงสำหรับการทดสอบหน่วยอย่างไรก็ตามไลบรารี OSS WireMock.netใช้งานง่ายและเร็วพอที่จะเรียกใช้พร้อมกับการทดสอบจำนวนมากดังนั้นจึงควรพิจารณา การตั้งค่าเป็นรหัสไม่กี่บรรทัด:

var server = FluentMockServer.Start();
server.Given(
      Request.Create()
      .WithPath("/some/thing").UsingGet()
   )
   .RespondWith(
       Response.Create()
       .WithStatusCode(200)
       .WithHeader("Content-Type", "application/json")
       .WithBody("{'attr':'value'}")
   );

คุณสามารถดูรายละเอียดเพิ่มเติมและคำแนะนำในการใช้ Wiremock ในการทดสอบได้ที่นี่


8

นี่เป็นวิธีง่ายๆที่ใช้ได้ผลดีสำหรับฉัน

การใช้ห้องสมุดจำลองขั้นต่ำ

// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
   .Protected()
   // Setup the PROTECTED method to mock
   .Setup<Task<HttpResponseMessage>>(
      "SendAsync",
      ItExpr.IsAny<HttpRequestMessage>(),
      ItExpr.IsAny<CancellationToken>()
   )
   // prepare the expected response of the mocked http call
   .ReturnsAsync(new HttpResponseMessage()
   {
      StatusCode = HttpStatusCode.OK,
      Content = new StringContent("[{'id':1,'value':'1'}]"),
   })
   .Verifiable();

// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
   BaseAddress = new Uri("http://test.com/"),
};

var subjectUnderTest = new MyTestClass(httpClient);

// ACT
var result = await subjectUnderTest
   .GetSomethingRemoteAsync('api/test/whatever');

// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);

// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");

handlerMock.Protected().Verify(
   "SendAsync",
   Times.Exactly(1), // we expected a single external request
   ItExpr.Is<HttpRequestMessage>(req =>
      req.Method == HttpMethod.Get  // we expected a GET request
      && req.RequestUri == expectedUri // to this uri
   ),
   ItExpr.IsAny<CancellationToken>()
);

ที่มา: https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/


ฉันยังใช้สิ่งนี้สำเร็จ ฉันชอบที่จะใช้สิ่งนี้ในการแยกส่วนในการพึ่งพา nuget และคุณได้เรียนรู้เพิ่มเติมเกี่ยวกับสิ่งที่เกิดขึ้นภายใต้ประทุนด้วย สิ่งที่ดีคือวิธีการส่วนใหญ่จบลงด้วยการใช้งานSendAsyncดังนั้นไม่จำเป็นต้องตั้งค่าเพิ่มเติม
Steve Pettifer

4

ฉันไม่มั่นใจในหลาย ๆ คำตอบ

HttpClientครั้งแรกของทั้งหมดคิดว่าคุณต้องการที่จะใช้วิธีการทดสอบหน่วยที่ใช้ คุณไม่ควรสร้างอินสแตนซ์HttpClientโดยตรงในการใช้งานของคุณ คุณควรฉีดโรงงานด้วยความรับผิดชอบในการจัดหาตัวอย่างHttpClientให้คุณ ด้วยวิธีนี้คุณสามารถเยาะเย้ยโรงงานนั้นในภายหลังและส่งคืนตามที่HttpClientคุณต้องการ (เช่นจำลองHttpClientไม่ใช่ของจริง)

ดังนั้นคุณจะมีโรงงานดังต่อไปนี้:

public interface IHttpClientFactory
{
    HttpClient Create();
}

และการใช้งาน:

public class HttpClientFactory
    : IHttpClientFactory
{
    public HttpClient Create()
    {
        var httpClient = new HttpClient();
        return httpClient;
    }
}

แน่นอนคุณจะต้องลงทะเบียนในคอนเทนเนอร์ IoC ของคุณการใช้งานนี้ หากคุณใช้ Autofac มันจะเป็นดังนี้:

builder
    .RegisterType<IHttpClientFactory>()
    .As<HttpClientFactory>()
    .SingleInstance();

ตอนนี้คุณจะมีการใช้งานที่เหมาะสมและทดสอบได้ ลองนึกภาพว่าวิธีการของคุณเป็นดังนี้:

public class MyHttpClient
    : IMyHttpClient
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<string> PostAsync(Uri uri, string content)
    {
        using (var client = _httpClientFactory.Create())
        {
            var clientAddress = uri.GetLeftPart(UriPartial.Authority);
            client.BaseAddress = new Uri(clientAddress);
            var content = new StringContent(content, Encoding.UTF8, "application/json");
            var uriAbsolutePath = uri.AbsolutePath;
            var response = await client.PostAsync(uriAbsolutePath, content);
            var responseJson = response.Content.ReadAsStringAsync().Result;
            return responseJson;
        }
    }
}

ตอนนี้ส่วนการทดสอบ HttpClientขยายออกไปHttpMessageHandlerซึ่งเป็นนามธรรม มาสร้าง "จำลอง" HttpMessageHandlerว่ายอมรับผู้รับมอบสิทธิ์เพื่อที่เมื่อเราใช้การจำลองเราสามารถตั้งค่าพฤติกรรมแต่ละอย่างสำหรับการทดสอบแต่ละครั้งได้

public class MockHttpMessageHandler 
    : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;

    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
    {
        _sendAsyncFunc = sendAsyncFunc;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _sendAsyncFunc.Invoke(request, cancellationToken);
    }
}

และตอนนี้และด้วยความช่วยเหลือของ Moq (และ FluentAssertions ซึ่งเป็นไลบรารีที่ทำให้การทดสอบหน่วยอ่านง่ายขึ้น) เรามีทุกสิ่งที่จำเป็นในการทดสอบหน่วยวิธี PostAsync ของเราที่ใช้ HttpClient

public static class PostAsyncTests
{
    public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
        : Given_WhenAsync_Then_Test
    {
        private SalesOrderHttpClient _sut;
        private Uri _uri;
        private string _content;
        private string _expectedResult;
        private string _result;

        protected override void Given()
        {
            _uri = new Uri("http://test.com/api/resources");
            _content = "{\"foo\": \"bar\"}";
            _expectedResult = "{\"result\": \"ok\"}";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var messageHandlerMock =
                new MockHttpMessageHandler((request, cancellation) =>
                {
                    var responseMessage =
                        new HttpResponseMessage(HttpStatusCode.Created)
                        {
                            Content = new StringContent("{\"result\": \"ok\"}")
                        };

                    var result = Task.FromResult(responseMessage);
                    return result;
                });

            var httpClient = new HttpClient(messageHandlerMock);
            httpClientFactoryMock
                .Setup(x => x.Create())
                .Returns(httpClient);

            var httpClientFactory = httpClientFactoryMock.Object;

            _sut = new SalesOrderHttpClient(httpClientFactory);
        }

        protected override async Task WhenAsync()
        {
            _result = await _sut.PostAsync(_uri, _content);
        }


        [Fact]
        public void Then_It_Should_Return_A_Valid_JsonMessage()
        {
            _result.Should().BeEquivalentTo(_expectedResult);
        }
    }
}

เห็นได้ชัดว่าการทดสอบนี้โง่และเรากำลังทดสอบการเยาะเย้ยของเราจริงๆ แต่คุณได้รับความคิด คุณควรทดสอบตรรกะที่มีความหมายขึ้นอยู่กับการใช้งานของคุณเช่น ..

  • หากสถานะรหัสของการตอบกลับไม่ใช่ 201 ควรมีข้อยกเว้นหรือไม่
  • หากไม่สามารถแยกวิเคราะห์ข้อความตอบกลับจะเกิดอะไรขึ้น?
  • เป็นต้น

คำตอบนี้มีจุดประสงค์เพื่อทดสอบบางสิ่งที่ใช้ HttpClient และนี่เป็นวิธีที่ดีในการทำเช่นนั้น


4

เข้าร่วมปาร์ตี้ช้าไปหน่อย แต่ฉันชอบใช้ wiremocking ( https://github.com/WireMock-Net/WireMock.Net ) เมื่อใดก็ตามที่เป็นไปได้ในการทดสอบการผสานรวมของไมโครบริการหลักดอทเน็ตพร้อมการอ้างอิง REST แบบดาวน์สตรีม

ด้วยการใช้ TestHttpClientFactory เพื่อขยาย IHttpClientFactory เราสามารถแทนที่วิธีการ

HttpClient CreateClient (ชื่อสตริง)

ดังนั้นเมื่อใช้ไคลเอนต์ที่มีชื่อภายในแอปของคุณคุณจะสามารถควบคุมการส่งคืน HttpClient แบบต่อสายไปยัง Wiremock ของคุณได้

สิ่งที่ดีเกี่ยวกับแนวทางนี้คือคุณไม่ได้เปลี่ยนแปลงสิ่งใดภายในแอปพลิเคชันที่คุณกำลังทดสอบและเปิดใช้งานการทดสอบการรวมหลักสูตรโดยทำคำขอ REST จริงกับบริการของคุณและล้อเลียน json (หรืออะไรก็ตาม) ที่คำขอดาวน์สตรีมจริงควรส่งคืน สิ่งนี้นำไปสู่การทดสอบที่รัดกุมและการเยาะเย้ยน้อยที่สุดในใบสมัครของคุณ

    public class TestHttpClientFactory : IHttpClientFactory 
{
    public HttpClient CreateClient(string name)
    {
        var httpClient = new HttpClient
        {
            BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
            // G.Config is our singleton config access, so the endpoint 
            // to the running wiremock is used in the test
        };
        return httpClient;
    }
}

และ

// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);

2

คุณสามารถใช้ไลบรารีRichardSzalay MockHttpซึ่งจำลอง HttpMessageHandler และสามารถส่งคืนวัตถุ HttpClient เพื่อใช้ในระหว่างการทดสอบ

GitHub จำลอง Http

PM> ติดตั้งแพ็คเกจ RichardSzalay.MockHttp

จากเอกสาร GitHub

MockHttp กำหนด HttpMessageHandler ทดแทนซึ่งเป็นเอ็นจิ้นที่ขับเคลื่อน HttpClient ซึ่งมี API การกำหนดค่าที่คล่องแคล่วและให้การตอบกลับสำเร็จรูป ผู้โทร (เช่นชั้นบริการของแอปพลิเคชันของคุณ) ยังคงไม่ทราบว่ามีอยู่

ตัวอย่างจาก GitHub

 var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

var json = await response.Content.ReadAsStringAsync();

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}

2

เนื่องจากHttpClientใช้SendAsyncวิธีการดำเนินการทั้งหมดHTTP Requestsคุณสามารถoverride SendAsyncวิธีการและล้อเลียนไฟล์HttpClient.

สำหรับการห่อนั้นสร้างHttpClientเป็น a มีinterfaceลักษณะดังนี้

public interface IServiceHelper
{
    HttpClient GetClient();
}

จากนั้นใช้ด้านบนinterfaceสำหรับการฉีดพึ่งพาในบริการของคุณตัวอย่างด้านล่าง

public class SampleService
{
    private readonly IServiceHelper serviceHelper;

    public SampleService(IServiceHelper serviceHelper)
    {
        this.serviceHelper = serviceHelper;
    }

    public async Task<HttpResponseMessage> Get(int dummyParam)
    {
        try
        {
            var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
            var client = serviceHelper.GetClient();
            HttpResponseMessage response = await client.GetAsync(dummyUrl);               

            return response;
        }
        catch (Exception)
        {
            // log.
            throw;
        }
    }
}

SendAsyncตอนนี้ในโครงการทดสอบหน่วยสร้างชั้นช่วยสำหรับการเยาะเย้ย นี่คือFakeHttpResponseHandlerคลาสที่inheriting DelegatingHandlerจะให้ตัวเลือกในการแทนที่SendAsyncเมธอด หลังจากลบล้างSendAsyncเมธอดแล้วจำเป็นต้องตั้งค่าการตอบสนองสำหรับแต่ละวิธีที่HTTP RequestเรียกSendAsyncใช้สำหรับสิ่งนั้นให้สร้างDictionaryด้วยkeyas Uriและvalueas HttpResponseMessageดังนั้นเมื่อใดก็ตามที่มีHTTP RequestและหากการUriจับคู่SendAsyncจะส่งคืนค่าที่กำหนดHttpResponseMessageไว้

public class FakeHttpResponseHandler : DelegatingHandler
{
    private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
    private readonly JavaScriptSerializer javaScriptSerializer;
    public FakeHttpResponseHandler()
    {
        fakeServiceResponse =  new Dictionary<Uri, HttpResponseMessage>();
        javaScriptSerializer =  new JavaScriptSerializer();
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
    {
        fakeServiceResponse.Remove(uri);
        fakeServiceResponse.Add(uri, httpResponseMessage);
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    /// <param name="requestParameter">Query string parameter.</param>
    public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter)
    {
        var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter);
        var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter));
        fakeServiceResponse.Remove(actualUri);
        fakeServiceResponse.Add(actualUri, httpResponseMessage);
    }

    // all method in HttpClient call use SendAsync method internally so we are overriding that method here.
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if(fakeServiceResponse.ContainsKey(request.RequestUri))
        {
            return Task.FromResult(fakeServiceResponse[request.RequestUri]);
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            RequestMessage = request,
            Content = new StringContent("Not matching fake found")
        });
    }
}

สร้างการใช้งานใหม่IServiceHelperโดยจำลองกรอบงานหรือชอบด้านล่าง FakeServiceHelperคลาสนี้เราสามารถใช้ฉีดFakeHttpResponseHandlerคลาสเพื่อที่ว่าเมื่อใดก็ตามที่HttpClientสร้างขึ้นโดยคลาสนี้classจะใช้FakeHttpResponseHandler classแทนการนำไปใช้จริง

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

    public FakeServiceHelper(DelegatingHandler delegatingHandler)
    {
        this.delegatingHandler = delegatingHandler;
    }

    public HttpClient GetClient()
    {
        return new HttpClient(delegatingHandler);
    }
}

และในการทดสอบการกำหนดค่าFakeHttpResponseHandler classโดยการเพิ่มและคาดว่าUri ควรจะเกิดขึ้นจริงปลายทางเพื่อที่ว่าเมื่อวิธีการที่เรียกว่าจากที่เกิดขึ้นจริงการดำเนินการก็จะตรงกับในและตอบสนองกับการกำหนดค่า หลังจากกำหนดค่าให้ฉีดไปยังการใช้งานปลอม จากนั้นฉีดไปที่บริการจริงซึ่งจะทำให้บริการจริงใช้วิธีการHttpResponseMessageUriserviceUrioverridden SendAsyncserviceUriDictionaryHttpResponseMessageFakeHttpResponseHandler objectIServiceHelperFakeServiceHelper classoverride SendAsync

[TestClass]
public class SampleServiceTest
{
    private FakeHttpResponseHandler fakeHttpResponseHandler;

    [TestInitialize]
    public void Initialize()
    {
        fakeHttpResponseHandler = new FakeHttpResponseHandler();
    }

    [TestMethod]
    public async Task GetMethodShouldReturnFakeResponse()
    {
        Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
        const int dummyParam = 123456;
        const string expectdBody = "Expected Response";

        var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectdBody)
        };

        fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);

        var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);

        var sut = new SampleService(fakeServiceHelper);

        var response = await sut.Get(dummyParam);

        var responseBody = await response.Content.ReadAsStringAsync();

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(expectdBody, responseBody);
    }
}

GitHub Link: ซึ่งมีตัวอย่างการใช้งาน


แม้ว่ารหัสนี้อาจช่วยแก้ปัญหาได้รวมถึงคำอธิบายว่าทำไมจึงแก้ปัญหานี้ได้จะช่วยปรับปรุงคุณภาพของโพสต์ของคุณได้อย่างแท้จริงและอาจส่งผลให้มีการโหวตเพิ่มขึ้น โปรดจำไว้ว่าคุณกำลังตอบคำถามสำหรับผู้อ่านในอนาคตไม่ใช่แค่คนที่ถามตอนนี้ โปรดแก้ไขคำตอบของคุณเพื่อเพิ่มคำอธิบายและระบุข้อ จำกัด และสมมติฐานที่ใช้
БогданОпир

ขอบคุณ @ БогданОпирสำหรับคำติชมที่อัปเดตคำอธิบาย
ghosh-arun

1

นี่เป็นคำถามเก่า แต่ฉันรู้สึกอยากจะขยายคำตอบด้วยวิธีแก้ปัญหาที่ฉันไม่เห็นที่นี่
คุณสามารถปลอม Microsoft assemly (System.Net.Http) จากนั้นใช้ ShinsContext ในระหว่างการทดสอบ

  1. ใน VS 2017 คลิกขวาที่แอสเซมบลี System.Net.Http และเลือก "Add Fakes Assembly"
  2. ใส่รหัสของคุณในวิธีการทดสอบหน่วยภายใต้ ShimsContext.Create () โดยใช้ ด้วยวิธีนี้คุณสามารถแยกรหัสที่คุณวางแผนจะปลอม HttpClient
  3. ขึ้นอยู่กับการใช้งานและการทดสอบของคุณฉันขอแนะนำให้ใช้การแสดงที่ต้องการทั้งหมดซึ่งคุณเรียกใช้เมธอดบน HttpClient และต้องการปลอมค่าที่ส่งคืน การใช้ ShimHttpClient AllInstances จะปลอมการใช้งานของคุณในทุกอินสแตนซ์ที่สร้างขึ้นระหว่างการทดสอบของคุณ ตัวอย่างเช่นหากคุณต้องการปลอมเมธอด GetAsync () ให้ทำดังต่อไปนี้:

    [TestMethod]
    public void FakeHttpClient()
    {
        using (ShimsContext.Create())
        {
            System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) =>
            {
              //Return a service unavailable response
              var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
              var task = Task.FromResult(httpResponseMessage);
              return task;
            };
    
            //your implementation will use the fake method(s) automatically
            var client = new Connection(_httpClient);
            client.doSomething(); 
        }
    }

1

ฉันทำอะไรที่เรียบง่ายมากเพราะฉันอยู่ในสภาพแวดล้อม DI

public class HttpHelper : IHttpHelper
{
    private ILogHelper _logHelper;

    public HttpHelper(ILogHelper logHelper)
    {
        _logHelper = logHelper;
    }

    public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
    {
        HttpResponseMessage response;
        using (var client = new HttpClient())
        {
            if (headers != null)
            {
                foreach (var h in headers)
                {
                    client.DefaultRequestHeaders.Add(h.Key, h.Value);
                }
            }
            response = await client.GetAsync(uri);
        }

        return response;
    }

    public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
    {
        ...

        rawResponse = await GetAsync(uri, headers);

        ...
    }

}

และการเยาะเย้ยคือ:

    [TestInitialize]
    public void Initialize()
    {
       ...
        _httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
       ...
    }

    [TestMethod]
    public async Task SuccessStatusCode_WithAuthHeader()
    {
        ...

        _httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
            Task<HttpResponseMessage>.Factory.StartNew(() =>
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(_testData))
                };
            })
        );
        var result = await _httpHelper.Object.GetAsync<TestDTO>(...);

        Assert.AreEqual(...);
    }

1

สิ่งที่คุณต้องมีคือHttpMessageHandlerคลาสเวอร์ชันทดสอบที่คุณส่งผ่านไปยังHttpClientctor ประเด็นหลักคือHttpMessageHandlerชั้นเรียนทดสอบของคุณจะมีHttpRequestHandlerตัวแทนที่ผู้โทรสามารถกำหนดและจัดการHttpRequestในแบบที่พวกเขาต้องการได้

public class FakeHttpMessageHandler : HttpMessageHandler
    {
        public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
        (r, c) => 
            new HttpResponseMessage
            {
                ReasonPhrase = r.RequestUri.AbsoluteUri,
                StatusCode = HttpStatusCode.OK
            };


        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(HttpRequestHandler(request, cancellationToken));
        }
    }

คุณสามารถใช้อินสแตนซ์ของคลาสนี้เพื่อสร้างอินสแตนซ์ HttpClient ที่เป็นรูปธรรม ผ่านตัวแทน HttpRequestHandler คุณสามารถควบคุมคำขอ http ขาออกจาก HttpClient ได้อย่างเต็มที่


1

แรงบันดาลใจจากคำตอบ PointZeroTwo ของที่นี่เป็นตัวอย่างการใช้NUnitและFakeItEasy

SystemUnderTest ในตัวอย่างนี้คือคลาสที่คุณต้องการทดสอบ - ไม่มีเนื้อหาตัวอย่างให้ แต่ฉันถือว่าคุณมีแล้ว!

[TestFixture]
public class HttpClientTests
{
    private ISystemUnderTest _systemUnderTest;
    private HttpMessageHandler _mockMessageHandler;

    [SetUp]
    public void Setup()
    {
        _mockMessageHandler = A.Fake<HttpMessageHandler>();
        var httpClient = new HttpClient(_mockMessageHandler);

        _systemUnderTest = new SystemUnderTest(httpClient);
    }

    [Test]
    public void HttpError()
    {
        // Arrange
        A.CallTo(_mockMessageHandler)
            .Where(x => x.Method.Name == "SendAsync")
            .WithReturnType<Task<HttpResponseMessage>>()
            .Returns(Task.FromResult(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.InternalServerError,
                Content = new StringContent("abcd")
            }));

        // Act
        var result = _systemUnderTest.DoSomething();

        // Assert
        // Assert.AreEqual(...);
    }
}

จะเกิดอะไรขึ้นถ้าฉันต้องการส่งผ่านพารามิเตอร์ไปยังเมธอดที่กล่าวถึง "x.Method.Name" .. ?
Shailesh

1

หลังจากค้นหาอย่างรอบคอบแล้วฉันก็พบแนวทางที่ดีที่สุดในการทำสิ่งนี้ให้สำเร็จ

    private HttpResponseMessage response;

    [SetUp]
    public void Setup()
    {
        var handlerMock = new Mock<HttpMessageHandler>();

        handlerMock
           .Protected()
           .Setup<Task<HttpResponseMessage>>(
              "SendAsync",
              ItExpr.IsAny<HttpRequestMessage>(),
              ItExpr.IsAny<CancellationToken>())
           // This line will let you to change the response in each test method
           .ReturnsAsync(() => response);

        _httpClient = new HttpClient(handlerMock.Object);

        yourClinet = new YourClient( _httpClient);
    }

อย่างที่คุณสังเกตเห็นว่าฉันใช้แพ็คเกจ Moq และ Moq.Protected


0

บางทีอาจมีรหัสบางอย่างที่จะเปลี่ยนแปลงในโครงการปัจจุบันของคุณ แต่สำหรับโครงการใหม่คุณควรพิจารณาใช้ Flurl

https://flurl.dev

เป็นไลบรารีไคลเอ็นต์ HTTP สำหรับ. NET พร้อมอินเทอร์เฟซที่คล่องแคล่วซึ่งเปิดใช้งานการทดสอบโดยเฉพาะสำหรับโค้ดที่ใช้เพื่อสร้างคำขอ HTTP

มีตัวอย่างโค้ดมากมายบนเว็บไซต์ แต่สรุปสั้น ๆ ว่าคุณใช้โค้ดนี้ในโค้ดของคุณ

เพิ่มการใช้งาน

using Flurl;
using Flurl.Http;

ส่งคำขอรับและอ่านคำตอบ

public async Task SendGetRequest()
{
   var response = await "https://example.com".GetAsync();
   // ...
}

ในการทดสอบหน่วย Flurl จะทำหน้าที่เหมือนการจำลองที่สามารถกำหนดค่าให้ทำงานได้ตามต้องการและยังตรวจสอบการโทรที่ทำ

using (var httpTest = new HttpTest())
{
   // Arrange
   httpTest.RespondWith("OK", 200);

   // Act
   await sut.SendGetRequest();

   // Assert
   httpTest.ShouldHaveCalled("https://example.com")
      .WithVerb(HttpMethod.Get);
}

0

เพื่อเพิ่ม 2 เซ็นต์ของฉัน เพื่อล้อเลียนวิธีการขอ http เฉพาะไม่ว่าจะเป็นรับหรือโพส สิ่งนี้ได้ผลสำหรับฉัน

mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
                                                .Returns(Task.FromResult(new HttpResponseMessage()
                                                {
                                                    StatusCode = HttpStatusCode.OK,
                                                    Content = new StringContent(""),
                                                })).Verifiable();
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.