نظرات مطالب
iTextSharp و نمایش صحیح تاریخ در متنی راست به چپ
سلام، می خواستم اگر ممکن باشه من رو راهنمایی کنید، من می خوام با استفاده از itextsharp محتوای یک فایل پی دی اف رو به صورت txt ذخیره کنم، با زبانهای چپ به راست خیلی آسون هست ولی در زبان راست به چپ، کلمات از اخر به اول نوشته می شن مثلا کلمه ارزیابی به صورت "یبایزرا" خوانده می شه، ممنون می شم من رو راهنمایی بکنید.
        private static string ParsPDFToString()
        {
            PdfReader reader = new PdfReader("c:/2v.pdf");
            PdfReaderContentParser parser = new PdfReaderContentParser(reader);
            StringBuilder sb = new StringBuilder();
            Console.WriteLine("reader.NumberOfPages : " + reader.NumberOfPages.ToString());
            Console.ReadKey();
            for (int i = 1; i <= reader.NumberOfPages; i++)
            {
                ITextExtractionStrategy strategy = parser.ProcessContent(
                  i , new SimpleTextExtractionStrategy()
                );
                sb.Append(strategy.GetResultantText());
            }
            return sb.ToString();
        }
نظرات مطالب
iTextSharp و نمایش صحیح تاریخ در متنی راست به چپ
سلام، می خواستم اگر ممکن باشه من رو راهنمایی کنید، من می خوام با استفاده از itextsharp محتوای یک فایل پی دی اف رو به صورت txt ذخیره کنم، با زبانهای چپ به راست خیلی آسون هست ولی در زبان راست به چپ، کلمات از اخر به اول نوشته می شن مثلا کلمه ارزیابی به صورت "یبایزرا" خوانده می شه، ممنون می شم من رو راهنمایی بکنید.
        private static string ParsPDFToString()
        {
            PdfReader reader = new PdfReader("c:/2v.pdf");
            PdfReaderContentParser parser = new PdfReaderContentParser(reader);
            StringBuilder sb = new StringBuilder();
            Console.WriteLine("reader.NumberOfPages : " + reader.NumberOfPages.ToString());
            Console.ReadKey();
            for (int i = 1; i <= reader.NumberOfPages; i++)
            {
                ITextExtractionStrategy strategy = parser.ProcessContent(
                  i , new SimpleTextExtractionStrategy()
                );
                sb.Append(strategy.GetResultantText());
            }
            return sb.ToString();
        }
نظرات مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
یک نکته‌ی تکمیلی: همیشه بهتر است در سمت کلاینت هم تاریخ انقضای JWT پیش از استفاده بررسی شود
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorWasm.Client.Utils
{
    public class JwtInfo
    {
        public IEnumerable<Claim> Claims { set; get; }

        public DateTime? ExpirationDateUtc { set; get; }

        public bool IsExpired { set; get; }

        public IEnumerable<string> Roles { set; get; }
    }

    /// <summary>
    /// From the Steve Sanderson’s Mission Control project:
    /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
    /// </summary>
    public static class JwtParser
    {
        public static JwtInfo ParseClaimsFromJwt(string jwt)
        {
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];

            var jsonBytes = getBase64WithoutPadding(payload);

            foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes))
            {
                if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
                {
                    foreach (var itemValue in element.EnumerateArray())
                    {
                        claims.Add(new Claim(keyValue.Key, itemValue.ToString()));
                    }
                }
                else
                {
                    claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString()));
                }
            }

            var roles = getRoles(claims);
            var expirationDateUtc = getDateUtc(claims, "exp");
            var isExpired = getIsExpired(expirationDateUtc);
            return new JwtInfo
            {
                Claims = claims,
                Roles = roles,
                ExpirationDateUtc = expirationDateUtc,
                IsExpired = isExpired
            };
        }

        private static IList<string> getRoles(IList<Claim> claims) =>
            claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();

        private static byte[] getBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }

        private static bool getIsExpired(DateTime? expirationDateUtc) =>
            !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow);

        private static DateTime? getDateUtc(IList<Claim> claims, string type)
        {
            var exp = claims.SingleOrDefault(claim => claim.Type == type);
            if (exp == null)
            {
                return null;
            }

            var expValue = getTimeValue(exp.Value);
            if (expValue == null)
            {
                return null;
            }

            var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            return dateTimeEpoch.AddSeconds(expValue.Value);
        }

        private static long? getTimeValue(string claimValue)
        {
            if (long.TryParse(claimValue, out long resultLong))
                return resultLong;

            if (float.TryParse(claimValue, out float resultFloat))
                return (long)resultFloat;

            if (double.TryParse(claimValue, out double resultDouble))
                return (long)resultDouble;

            return null;
        }
    }
}
مطالب
نحوه استفاده از Text template ها در دات نت - قسمت اول
 یکی از امکانات کمتر شناخته شده در دات نت، امکان تولید اتوماتیک کد (Code Generator)، توسط فایلهایی به عنوان Text template می‌باشد. اگر چه فایلهای Text template با پسوند tt  از Visual Studio 2008 بطور آشکار به IDE اضافه گردیده‌اند، این امکان پیش از این نیز بصورت توکار در Visual Studio جهت تولید کد دات نت برای ابزارهایی مانند Dataset ، Report  و ... وجود داشته است. در حال حاضر Visual Studio بصورت توکار  از این امکان برای تولیدکلاسها و  کد‌های EntityFramework ( edmx ) ،dbml  ، Dataset  و ... استفاده می‌نماید. 
شما نیز با دانستن قواعد ساده کد نویسی برای Text template‌ها می‌توانید از این امکان برای تولید اتوماتیک کلاسها، HTML ، XML  و بطور کلی هر نوع فایل متنی استفاده نمایید. کاربردهای عملی Text template بیشتر برای تولید کد از روی دیتابیس و مثلا بر اساس فیلد‌های هر جدول و امثال آن می‌باشد. اما با کمی خلاقیت استفاده‌های بسیار زیاد و جالبی را می‌توانید از آن مشاهده نمایید.
کاربردهایی مانند : تولید خود کار فایل Config، تولید خودکار  Unit Test، تولید خودکار کلاسهای CRUD برای هر موجودیت در لایه Data، تولید خودکار SQL برای StoredProcedure ها، تولید خودکار Html و Js، تولید خودکار فرمهای ASPX و ... برای فرمهای ثابت و تکراری با فرآیند مشخص، تولید خودکار مستندات پروژه در قالب Word Document  , …  ،  تولید خودکار فایلهای Excel از اطلاعات خاص و تولید خودکار فرمهای xaml و .....

در این آموزش با قواعد اصلی نوشتن کد برای Text templateها آشنا می‌شویم و چند نمونه از کاربردهای آن را به عنوان مثال کاربردی مورد بررسی قرار خواهیم داد.
برای شروع قبل از توضیح در مورد قواعد کد نویسی Text template، یک فایل Text template ایجاد نمایید. برای ایجاد، از پنجره Add New Item  یک فایل Text template به پروژه اضافه نمایید:
 


توجه داشته باشید که نام فایل خروجی دقیقا هم نام با فایل tt مشخص شده خواهد بود. اما پسوند آن بسته به تنظیمات داخل آن متفاوت است. با اضافه کردن Text template داخل فایل، باید کد‌های زیر را مشاهده نمایید؛ در غیر این صورت آن را بنویسید:
 <#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>
Language زبانی را مشخص می‌کند که می‌خواهید با آن داخل فایل Text template کد نویسی نمایید. تعیین آن برای کامپایل الزامی است.
Extension نیز پسوند فایل خروجی تولید شده را مشخص می‌نماید . 
اگر در ادامه متن Hello dotnettips را بنویسید و فایل را Save کنید، کنار فایل tt مذکور، یک فایل با همان نام و پسوند txt ایجاد خواهد شد که داخل آن نوشته است:
Hello dotnettips

برای ایجاد فایل خروجی همچنین میتوانید روی فایل tt  کلیک راست نموده و از منوی باز شده  Run Custom Tool  را انتخاب نمایید. توجه داشته باشید که در تنظیمات Custom Tool باید مقدار  TextTemplatingFileGenerator وجود داشته باشد:
 

خوب؛ برای ادامه، باید با قواعد کد نویسی در Text template آشنا شوید. جلسه بعدی قواعد کد نویسی T4  را بررسی خواهیم کرد.

مطالب
Minimal API's در دات نت 6 - قسمت چهارم - تدارک مقدمات معماری بر اساس ویژگی‌ها
در معماری vertical slices با features سر و کار داریم؛ برای مثال برنامه‌ی ما دو ویژگی نویسنده‌ها و بلاگ‌ها را خواهد داشت و هر ویژگی، کاملا متکی به خود است. برای نمونه هر ویژگی می‌تواند به همراه یک ماژول باشد که به صورت مستقل، تمام سرویس‌ها، endpoints و میان‌افزارهای مورد نیاز خودش را ثبت می‌کند. در این معماری، تمام قسمت‌های مورد نیاز جهت کارکرد یک ویژگی، در کنار هم قرار می‌گیرند تا یافتن آن‌ها و درک ارتباطات بین آن‌ها ساده‌تر شود.


تعریف ساختار ماژول‌های ویژگی‌های معماری vertical slices

برای تعریف ساختار ماژولی که کار ثبت تمام نیازمندی‌های یک ویژگی را انجام می‌دهد، مانند ثبت سرویس‌ها، endpoints و میان‌افزارها، ابتدا پوشه‌ای به نام Contracts را به پروژه‌ی Api اضافه می‌کنیم؛ با این اینترفیس:
namespace MinimalBlog.Api.Contracts;

public interface IModule
{
    IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints);
}


ثبت خودکار ماژول‌های برنامه در ابتدای اجرای آن

پس از تعریف این قرارداد، اکنون می‌خواهیم هر ماژولی که در برنامه، اینترفیس فوق را پیاده سازی می‌کند، در ابتدای اجرای برنامه به صورت خودکار، یافت شده و اطلاعات آن به سیستم اضافه شود. برای این منظور متدهای الحاقی زیر را تعریف می‌کنیم:
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services,
        WebApplicationBuilder builder)
    {
        // ...

        builder.Services.AddAllModules(typeof(Program));

        return services;
    }

    private static void AddAllModules(this IServiceCollection services, params Type[] types)
    {
        // Using the `Scrutor` to add all of the application's modules at once.
        services.Scan(scan =>
            scan.FromAssembliesOf(types)
                .AddClasses(classes => classes.AssignableTo<IModule>())
                .AsImplementedInterfaces()
                .WithSingletonLifetime());
    }
}
این کلاس ساختار ساده‌ای دارد؛ ابتدا در متد AddAllModules، اسمبلی جاری جهت یافتن کلاس‌های پیاده سازی کننده‌ی اینترفیس IModule، اسکن می‌شود؛ با استفاده از کتابخانه‌ی Scrutor.
سپس کلاس‌های ثبت شده که هم اکنون جزئی از سیستم تزریق وابستگی‌های برنامه هستند، یافت شده و متد RegisterEndpoints آن‌ها فراخوانی می‌شوند تا دیگر نیازی نباشد به ازای هر ماژول، یکبار ثبت دستی این موارد در کلاس Program انجام شود.
using MinimalBlog.Api.Contracts;

namespace MinimalBlog.Api.Extensions;

public static class ModuleExtensions
{
    public static WebApplication RegisterEndpoints(this WebApplication app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        var modules = app.Services.GetServices<IModule>();
        foreach (var module in modules)
        {
            module.RegisterEndpoints(app);
        }

        return app;
    }
}
بنابراین در ادامه به کلاس Program مراجعه کرد و متد عمومی کلاس فوق را در آن به صورت app.RegisterEndpoints فراخوانی می‌کنیم:
using MinimalBlog.Api.Extensions;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices(builder);

var app = builder.Build();
app.ConfigureApplication();
app.RegisterEndpoints();

app.Run();
این چند سطر، کل محتوای فایل Program.cs برنامه را تشکیل می‌دهند.


ایجاد اولین Feature برنامه؛ ویژگی نویسندگان

برای تعریف اولین ویژگی برنامه که مختص به نویسندگان است، پوشه‌های جدید Features\Authors را در برنامه‌ی Api ایجاد می‌کنیم.
- اولین کاری را که در ادامه انجام خواهیم داد، انتقال فایل AuthorDto.cs که در قسمت قبل ایجاد کردیم، به درون این پوشه‌ی جدید است.
- سپس ماژول نویسندگان را به صورت زیر به آن اضافه می‌کنیم:
namespace MinimalBlog.Api.Features.Authors;

public class AuthorModule : IModule
{
    public IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
        {
            var authors = await ctx.Authors.ToListAsync();
            return authors;
        });

        endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
        {
            var author = new Author();
            author.FirstName = authorDto.FirstName;
            author.LastName = authorDto.LastName;
            author.Bio = authorDto.Bio;
            author.DateOfBirth = authorDto.DateOfBirth;

            ctx.Authors.Add(author);
            await ctx.SaveChangesAsync();

            return author;
        });

        return endpoints;
    }
}
در اینجا ماژول نویسندگان را که با پیاده سازی قرارداد IModule تشکیل شده‌است، مشاهده می‌کنید. در متد RegisterEndpoints آن، دو endpoints تعریف شده‌ی در کلاس Program برنامه را در قسمت قبل، Cut کرده و به اینجا منتقل کرده‌ایم. بنابراین اکنون کلاس Program، دیگر به همراه تعریف مستقیم هیچ endpoint ای نیست و خلوت شده‌است. هدف از Features هم دقیقا همین است تا هر ویژگی برنامه، متکی به خود بوده و مستقل باشد؛ به همراه تمام تعاریف مورد نیاز جهت کار با آن در یک محل مشخص (مانند انتقال فایل Dto مربوط به آن، به درون همین پوشه). مزیت این روش، درک ساده‌تر اجزای مرتبط و یافتن سریعتر ارتباطات قسمت‌های یک ویژگی خاص است. در آینده اگر مشکلی رخ داد و باگی بروز پیدا کرد، دقیقا می‌دانیم که محدوده‌ای که باید مورد بررسی قرار گیرد، کجاست و این محدوده، کوچک و متکی به خود است و در بین چندین پروژه‌ی مختلف، پراکنده نشده‌است.
کار نمونه سازی و اجرای متدهای این ماژول‌ها نیز توسط متدهای الحاقی کلاس ModuleExtensions، در ابتدای اجرای برنامه به صورت خودکار انجام می‌شود و نیازی به شلوغ کردن کلاس Program برای ثبت دستی آن‌ها نیست.


افزودن AutoMapper و MediatR به پروژه‌ی Api

در ادامه برای ساده سازی کار نگاشت‌های Dtoهای برنامه به مدل‌های دومین آن، از AutoMapper استفاده خواهیم کرد؛ همچنین از MediatR نیز برای پیاده سازی الگوی CQRS که در قسمت بعدی پیگیری خواهد شد. بنابراین در ابتدا بسته‌های نیوگت این دو را به پروژه‌ی Api اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />    
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />  
  </ItemGroup>
</Project>
سپس به کلاس ServiceCollectionExtensions مراجعه کرده و تعاریف ثبت سرویس‌های این دو را نیز اضافه می‌کنیم:
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services,
        WebApplicationBuilder builder)
    {
        // ...

        builder.Services.AddMediatR(typeof(Program));
        builder.Services.AddAutoMapper(typeof(Program));

        return services;
    }
}
اکنون می‌توان اولین Profile مربوط به AutoMapper را که کار نگاشت AuthorDto به Author و برعکس را انجام می‌دهد، به صورت زیر تهیه کنیم:
using AutoMapper;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class AuthorProfile : Profile
{
    public AuthorProfile()
    {
        CreateMap<AuthorDto, Author>().ReverseMap();
    }
}
این فایل نیز درون پوشه‌ی Features\Authors قرار می‌گیرد.
مطالب دوره‌ها
نگاهی به گزینه‌های مختلف مهیای جهت میزبانی SignalR
حداقل چهار گزینه برای Hosting سرویس‌های Hub برنامه‌های مبتنی بر SignalR وجود دارند که تا به اینجا، مورد دوم آن بیشتر بررسی گردید:
1) OWIN
2) ASP.NET Hosting
3) Self Hosting
4) Cloud و ویندوز Azure

1) OWIN
اگر به اسمبلی‌های همراه با SignalR دقت کنید، یکی از آن‌ها Microsoft.AspNet.SignalR.Owin.dll نام دارد. OWIN مخفف Open web server interface for .NET است و کار آن ایجاد لایه‌ای بین وب سرورها و برنامه‌های وب می‌باشد. یکی از اهداف مهم آن ترغیب دنیای سورس باز به تهیه ماژول‌های مختلف قابل استفاده در وب سرورهای دات نتی است. نکته‌ی مهمی که در SignalR و کلیه میزبان‌های آن وجود دارد، بنا شدن تمامی آن‌ها برفراز OWIN می‌باشد.

2) ASP.NET Hosting
بدون شک، میزبانی ASP.NET از هاب‌های SignalR، مرسوم‌ترین روش استفاده از این فناوری می‌باشد. این نوع میزبانی نیز برفراز OWIN بنا شده است. نصب آن توسط اجرای دستور پاور شل ذیل در یک پروژه وب صورت می‌گیرد:
 PM> Install-Package Microsoft.AspNet.SignalR

3) خود میزبانی یا Self hosting
خود میزبانی نیز برفراز OWIN تهیه شده است و برای پیاده سازی آن نیاز است وابستگی‌های مرتبط با آن، از طریق NuGet به کمک فرامین پاور شل ذیل دریافت شوند:
 PM> Install-Package Microsoft.AspNet.SignalR.Owin
PM> Install-Package Microsoft.Owin.Hosting -Pre
PM> Install-Package Microsoft.Owin.Host.HttpListener -Pre
مواردی که با پارامتر pre مشخص شده‌اند، در زمان نگارش این مطلب هنوز در مرحله بتا قرار دارند اما برای دموی برنامه کفایت می‌کنند.
مراحل تهیه یک برنامه ثالث (برای مثال خارج از IIS یا یک وب سرور آزمایشی) به عنوان میزبان Hubs مورد نیاز به این شرح هستند:
الف) کلاس آغازین میزبان باید با پیاده سازی اینترفیسی به نام IAppBuilder تهیه شود.
ب) مسیریابی‌های مورد نیاز تعریف گردند.
ج) وب سرور HTTP یا HTTPS توکار برای سرویس دهی آغاز گردد.

باید توجه داشت که در این حالت برخلاف روش ASP.NET Hosting، سایر اسمبلی‌های برنامه جهت یافتن Hubهای تعریف شده، اسکن نمی‌شوند. همچنین هنگام کار با jQuery مباحث عنوان شده در مورد تنظیم دسترسی‌های Cross domain نیز باید در اینجا اعمال گردند. به علاوه اجرای وب سرور توکار آن به دلایل امنیتی، نیاز به دسترسی مدیریتی دارد.

برای پیاده سازی یک نمونه، به برنامه‌ای که تاکنون تهیه کرده‌ایم، یک پروژه کنسول دیگر را به نام ConsoleHost اضافه کنید. البته باید درنظر داشت در دنیای واقعی این نوع برنامه‌ها را عموما از نوع سرویس‌های ویندوز NT تهیه می‌کنند.
در ادامه سه فرمان پاور شل یاد شده را برای افزودن وابستگی‌های مورد نیاز فراخوانی نمائید. همچنین باید دقت داشت که این دستور بر روی پروژه جدید اضافه شده باید اجرا گردد.
using System;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.Owin.Hosting;
using Owin;

namespace SignalR02.ConsoleHost
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapHubs(new HubConfiguration { EnableCrossDomain = true });
        }
    }

    [HubName("chat")]
    public class ChatHub : Hub
    {
        public void SendMessage(string message)
        {
            var msg = string.Format("{0}:{1}", Context.ConnectionId, message);
            Clients.All.hello(msg);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (WebApplication.Start<Startup>("http://localhost:1073/"))
            {
                Console.WriteLine("Press a key to terminate the server...");
                Console.Read();
            }
        }
    }
}
سپس یک کلاس Startup را با امضایی که مشاهده می‌کنید تهیه نمائید. در اینجا مسیریابی و تنظیمات دسترسی از سایر دومین‌ها مشخص شده‌اند. در ادامه یک Hub نمونه، تعریف و نهایتا توسط WebApplication.Start، این وب سرور راه اندازی می‌شود. اکنون اگر برنامه را اجرا کرده و به مسیر http://localhost:1073/signalr/hubs مراجعه کنید، فایل پروکسی تعاریف متادیتای مرتبط با سرور قابل مشاهده خواهد بود.
سمت کلاینت استفاده از آن هیچ تفاوتی نمی‌کند و با جزئیات آن پیشتر آشنا شده‌اید؛ برای مثال در کلاینت جی‌کوئری خاصیت connection.hub.url باید به مسیر جدید سرور هاب تنظیم گردد تا اتصالات به درستی برقرار شوند.


دریافت پروژه کامل مرتبط با این 4 قسمت (البته بدون فایل‌های باینری آن، جهت کاهش حجم 32 مگابایتی)
  SignalRSamples.zip
مطالب
آموزش QUnit #2
فریم ورک تست جاوا اسکریپت QUnit:
 انتخاب و استفاده از یک فریم ورک برای تست کد‌های جاوا اسکریپت، قطعا نتیجه بهتری را به همراه خواهد داشت. من در این جا از QUnit که یکی از بهترین‌های تست واحد است، استفاده می‌کنم. برای این کار فایل‌های qunit.js و qunit.css را دانلود و مانند زیر برای تست واحد آماده کنید:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Refactored date examples</title>

  <link rel="stylesheet" href="../qunit.css">
  <script src="../qunit.js"></script>
  <script src="prettydate.js"></script>
 
  <script>
  test("prettydate basics", function() {
    var now = "2013/01/28 22:25:00";
    equal(prettyDate(now, "2013/01/28 22:24:30"), "just now");
    equal(prettyDate(now, "201308/01/28 22:23:30"), "1 minute ago");
    equal(prettyDate(now, "2013/01/28 21:23:30"), "1 hour ago");
    equal(prettyDate(now, "2013/01/27 22:23:30"), "Yesterday");
    equal(prettyDate(now, "2013/01/26 22:23:30"), "2 days ago");
    equal(prettyDate(now, "2012/01/26 22:23:30"), undefined);
  });
  </script>
</head>
<body>
 
<div id="qunit"></div>
 
</body>
</html>
در کد بالا ابتدا فایل‌های فریم ورک و فایل prettydate.js را اضافه کردیم. برای نمایش نتیجه تست، یک تگ div با نام qunit در بین تگ body اضافه می‌کنیم. 
تابع test:
این تابع برای تست توابع نوشته شده، استفاده می‌شود. ورودی‌های این تابع، یکی عنوان تست و دومی یک متود دیگر، به عنوان ورودی دریافت می‌کند که در آن بدنه تست نوشته می‌شود.
تابع equal:
اولین تابع برای سنجش تست واحد equal است و در آن، تابعی که می‌خواهیم تست کنیم با مقدار خروجی آن مقایسه می‌شود.
فایل را با نام test.htm ذخیره و آن را در مرورگر خود باز نمایید. خروجی در شکل آورده شده است:

همین طور که در تصویر بزرگ می‌بینید اطلاعات مرورگر، زمان تکمیل تست و تعداد تست، تعداد تست پاس شده و تعداد تست شکست خورده، نشان داده شده است.

اگر یکی از تست‌ها با شکست روبرو شود رنگ پس زمینه قرمز و جزئیات شکست نمایش داده می‌شوند.
بهینه سازی، مرحله اول:
در حال حاضر تست ما کامل نیست زیرا امکان تست n weeks ago یا تعداد هفته پیش میسر نیست. قبل از آنکه این را به آزمون اضافه کنیم، تغییراتی در تست می‌دهیم
test("prettydate basics", function() {
  function date(then, expected) {
    equal(prettyDate("2013/01/28 22:25:00", then), expected);
  }
  date("2013/01/28 22:24:30", "just now");
  date("2013/01/28 22:23:30", "1 minute ago");
  date("2013/01/28 21:23:30", "1 hour ago");
  date("2013/01/27 22:23:30", "Yesterday");
  date("2013/01/26 22:23:30", "2 days ago");
  date("2012/01/26 22:23:30", undefined);
});
تابع prettyDate را در تابع دیگری به نام date قرار می‌دهیم. این تغییر سبب می‌شود تا امکان مقایسه زمان ورودی تست جاری با تست قبلی فراهم شود.
تست دستکاری عناصر DOM:
تا اینجا با تست توایع آشنا شدید، حالا می‌خواهیم تغییراتی در prettyDate دهیم تا امکان انتخاب عناصر DOM و به روزرسانی آن نیز وجود داشته باشد. فایل prettyDate2.js در زیر آورده شده است:
var prettyDate = {
  format: function(now, time){
    var date = new Date(time || ""),
      diff = (((new Date(now)).getTime() - date.getTime()) / 1000),
      day_diff = Math.floor(diff / 86400);
 
    if ( isNaN(day_diff) || day_diff < 0 || day_diff >= 31 )
      return;
 
    return day_diff === 0 && (
        diff < 60 && "just now" ||
        diff < 120 && "1 minute ago" ||
        diff < 3600 && Math.floor( diff / 60 ) +
          " minutes ago" ||
        diff < 7200 && "1 hour ago" ||
        diff < 86400 && Math.floor( diff / 3600 ) +
          " hours ago") ||
      day_diff === 1 && "Yesterday" ||
      day_diff < 7 && day_diff + " days ago" ||
      day_diff < 31 && Math.ceil( day_diff / 7 ) +
        " weeks ago";
  },
 
  update: function(now) {
    var links = document.getElementsByTagName("a");
    for ( var i = 0; i < links.length; i++ ) {
      if ( links[i].title ) {
        var date = prettyDate.format(now, links[i].title);
        if ( date ) {
          links[i].innerHTML = date;
        }
      }
    }
  }
};
prettyDate شامل دو تابع، یکی format که weeks ago به آن اضافه گردیده و تابع update که با انتخاب تگ‌ها، مقدار title را به تابع فرمت و خروجی آن را در Html هر عنصر قرار می‌دهد. حال یک تست واحد می‌نویسیم:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Refactored date examples</title>
  <link rel="stylesheet" href="../qunit.css">
  <script src="../qunit.js"></script>
  <script src="prettydate2.js"></script>
  <script>
  test("prettydate.format", function() {
    function date(then, expected) {
      equal(prettyDate.format("2013/01/28 22:25:00", then),
        expected);
    }
    date("2013/01/28 22:24:30", "just now");
    date("2013/01/28 22:23:30", "1 minute ago");
    date("2013/01/28 21:23:30", "1 hour ago");
    date("2013/01/27 22:23:30", "Yesterday");
    date("2013/01/26 22:23:30", "2 days ago");
    date("2012/01/26 22:23:30", undefined);
  });
 
  function domtest(name, now, first, second) {
    test(name, function() {
      var links = document.getElementById("qunit-fixture")
        .getElementsByTagName("a");
      equal(links[0].innerHTML, "January 28th, 2013");
      equal(links[2].innerHTML, "January 27th, 2013");
      prettyDate.update(now);
      equal(links[0].innerHTML, first);
      equal(links[2].innerHTML, second);
    });
  }
  domtest("prettyDate.update", "2013-01-28T22:25:00Z",
    "2 hours ago", "Yesterday");
  domtest("prettyDate.update, one day later", "2013/01/29 22:25:00",
    "Yesterday", "2 days ago");
  </script>
</head>
<body>
 
<div id="qunit"></div>
<div id="qunit-fixture">
 
<ul>
  <li id="post57">
    <p>blah blah blah...</p>
    <small>
      Posted <span>
        <a href="/2013/01/blah/57/" title="2013-01-28T20:24:17Z"
          >January 28th, 2013</a>
      </span>
      by <span><a href=""></a></span>
    </small>
  </li>
  <li id="post57">
    <p>blah blah blah...</p>
    <small>
      Posted <span>
        <a href="/2013/01/blah/57/" title="2013-01-27T22:24:17Z"
          >January 27th, 2013</a>
      </span>
      by <span><a href=""></a></span>
    </small>
  </li>
</ul>
 
</div>
 
</body>
</html>
همین طور که مشاهد می‌کنید در تست واحد اول خود تابع prettyDate.format را تست نموده ایم. در تست بعدی عناصر DOM نیز دستکاری و تست شده است. تابع domtest با جستجوی تگ qunit-fixture و تگ‌های a درون آن، مقدار نهایی html آن با مقدار داده شده، مقایسه شده است.

در شکل بالا نتیجه تست واحد نشان داده شده است.